• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

EsupPortail / Esup-Pod / 22574034458

02 Mar 2026 11:30AM UTC coverage: 68.109%. First build
22574034458

Pull #1406

github

web-flow
Merge 207dd0354 into ed8189ee5
Pull Request #1406: Enhance dashboard filtering and runner manager administration with i18n updates

70 of 145 new or added lines in 8 files covered. (48.28%)

12923 of 18974 relevant lines covered (68.11%)

0.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.83
/pod/video_encode_transcript/runner_manager.py
1
"""Runner Manager orchestration helpers for encoding and transcription tasks in Esup-Pod.
2

3
This module builds task payloads, dispatches them to available runner managers,
4
and keeps local task rows synchronized with runner-side task status.
5
"""
6

7
import json
1✔
8
import logging
1✔
9
import os
1✔
10
from typing import Any, Literal, Optional, TypeAlias, TypedDict, Union, cast
1✔
11

12
import requests
1✔
13
from django.conf import settings
1✔
14
from django.contrib.sites.models import Site
1✔
15
from django.shortcuts import get_object_or_404
1✔
16
from django.utils.translation import gettext_lazy as _
1✔
17
from pod.cut.models import CutVideo
1✔
18
from pod.recorder.models import Recording
1✔
19
from pod.video.models import Video
1✔
20
from pod.video_encode_transcript.models import RunnerManager, Task
1✔
21
from pod.video_encode_transcript.runner_manager_utils import (
1✔
22
    store_before_remote_encoding_recording,
23
    store_before_remote_encoding_video,
24
)
25

26
from .utils import change_encoding_step
1✔
27

28
if __name__ == "__main__":
1✔
29
    from encoding_utils import get_list_rendition
×
30
else:
31
    from .encoding_utils import get_list_rendition
1✔
32

33
log = logging.getLogger(__name__)
1✔
34

35
DEBUG = getattr(settings, "DEBUG", True)
1✔
36

37
# Settings for template customization
38
TEMPLATE_VISIBLE_SETTINGS = getattr(
1✔
39
    settings,
40
    "TEMPLATE_VISIBLE_SETTINGS",
41
    {
42
        "TITLE_SITE": "Pod",
43
        "TITLE_ETB": "University name",
44
        "LOGO_SITE": "img/logoPod.svg",
45
        "LOGO_ETB": "img/esup-pod.svg",
46
        "LOGO_PLAYER": "img/pod_favicon.svg",
47
        "LINK_PLAYER": "",
48
        "LINK_PLAYER_NAME": _("Home"),
49
        "FOOTER_TEXT": ("",),
50
        "FAVICON": "img/pod_favicon.svg",
51
        "CSS_OVERRIDE": "",
52
        "PRE_HEADER_TEMPLATE": "",
53
        "POST_FOOTER_TEMPLATE": "",
54
        "TRACKING_TEMPLATE": "",
55
    },
56
)
57
__TITLE_SITE__ = (
1✔
58
    TEMPLATE_VISIBLE_SETTINGS["TITLE_SITE"]
59
    if (TEMPLATE_VISIBLE_SETTINGS.get("TITLE_SITE"))
60
    else "Pod"
61
)
62
__TITLE_ETB__ = (
1✔
63
    TEMPLATE_VISIBLE_SETTINGS["TITLE_ETB"]
64
    if (TEMPLATE_VISIBLE_SETTINGS.get("TITLE_ETB"))
65
    else "University name"
66
)
67

68
VERSION = getattr(settings, "VERSION", "4.X")
1✔
69

70
SECURE_SSL_REDIRECT = getattr(settings, "SECURE_SSL_REDIRECT", False)
1✔
71

72

73
SourceType = Literal["video", "recording"]
1✔
74
TaskType = Literal["encoding", "studio", "transcription"]
1✔
75
ParametersDict: TypeAlias = dict[str, Any]
1✔
76
HeadersDict: TypeAlias = dict[str, str]
1✔
77

78

79
class RunnerManagerTaskPayload(TypedDict):
1✔
80
    """Task payload expected by the Runner Manager API."""
81

82
    etab_name: str
1✔
83
    app_name: str
1✔
84
    app_version: str
1✔
85
    task_type: TaskType
1✔
86
    source_url: str
1✔
87
    notify_url: str
1✔
88
    parameters: ParametersDict
1✔
89

90

91
class RunnerManagerResponse(TypedDict, total=False):
1✔
92
    """Relevant fields returned by the Runner Manager API."""
93

94
    task_id: str
1✔
95
    status: str
1✔
96

97

98
def _build_rendition_parameters() -> ParametersDict:
1✔
99
    """Return rendition parameters serialized for the runner payload."""
100
    list_rendition = get_list_rendition()
×
101
    str_resolution: dict[str, dict[str, Any]] = {
×
102
        str(k): {"resolution": v["resolution"], "encode_mp4": v["encode_mp4"]}
103
        for k, v in list_rendition.items()
104
    }
105
    return {"rendition": json.dumps(str_resolution)}
×
106

107

108
def _attach_cut_info(parameters: ParametersDict, video: Video) -> None:
1✔
109
    """Attach cut information to parameters if it exists for the given video."""
110
    try:
×
111
        cut_video = CutVideo.objects.get(video=video)
×
112
        str_cut_info = {
×
113
            "start": str(cut_video.start),
114
            "end": str(cut_video.end),
115
            "initial_duration": str(cut_video.duration),
116
        }
117
        parameters["cut"] = json.dumps(str_cut_info)
×
118
    except CutVideo.DoesNotExist:
×
119
        pass
×
120

121

122
def _attach_dressing_info(parameters: ParametersDict, video: Video) -> None:
1✔
123
    """Attach dressing information to parameters if available for the given video."""
124
    try:
×
125
        from pod.dressing.models import Dressing
×
126

127
        site = Site.objects.get_current()
×
128
        url_scheme = "https" if SECURE_SSL_REDIRECT else "http"
×
129
        base_url = url_scheme + "://" + site.domain
×
130

131
        str_dressing_info = {}
×
132
        if Dressing.objects.filter(videos=video).exists():
×
133
            log.info("Dressing found for video id: %s", video.id)
×
134
            dressing = Dressing.objects.get(videos=video)
×
135
            if dressing:
×
136
                if dressing.watermark:
×
137
                    log.info("Dressing watermark found")
×
138
                    watermark_content_url = "%s/media/%s" % (
×
139
                        base_url,
140
                        str(dressing.watermark.file.name),
141
                    )
142
                    str_dressing_info["watermark"] = watermark_content_url
×
143
                    str_dressing_info["watermark_position"] = dressing.position
×
144
                    str_dressing_info["watermark_opacity"] = str(dressing.opacity)
×
145
                if dressing.opening_credits:
×
146
                    log.info("Dressing opening credits found")
×
147
                    str_dressing_info["opening_credits"] = dressing.opening_credits.slug
×
148
                    opening_content_url = "%s/media/%s" % (
×
149
                        base_url,
150
                        str(dressing.opening_credits.video.name),
151
                    )
152
                    str_dressing_info["opening_credits_video"] = opening_content_url
×
153
                    str_dressing_info["opening_credits_video_duration"] = str(
×
154
                        dressing.opening_credits.duration
155
                    )
156
                if dressing.ending_credits:
×
157
                    log.info("Dressing ending credits found")
×
158
                    str_dressing_info["ending_credits"] = dressing.ending_credits.slug
×
159
                    ending_content_url = "%s/media/%s" % (
×
160
                        base_url,
161
                        str(dressing.ending_credits.video.name),
162
                    )
163
                    str_dressing_info["ending_credits_video"] = ending_content_url
×
164
                    str_dressing_info["ending_credits_video_duration"] = str(
×
165
                        dressing.ending_credits.duration
166
                    )
167
                    # str_dressing_info["ending_credits_video_hasaudio"] = str(
168
                    #     dressing.ending_credits.video.has_audio()
169
                    # )
170
        if str_dressing_info:
×
171
            parameters["dressing"] = json.dumps(str_dressing_info)
×
172
    except Exception as exc:
×
173
        log.error(f"Error obtaining dressing for video {video.id}: {str(exc)}")
×
174

175

176
def _prepare_encoding_parameters(
1✔
177
    video: Optional[Video] = None,
178
) -> ParametersDict:
179
    """Prepare encoding parameters for video or recording.
180

181
    Args:
182
        video: Video object (for video encoding).
183
               For studio recordings, pass None as cut info doesn't apply.
184

185
    Returns:
186
        Dictionary with rendition and optionally cut information
187
    """
188
    parameters = _build_rendition_parameters()
×
189

190
    if video:
×
191
        _attach_cut_info(parameters, video)
×
192
        _attach_dressing_info(parameters, video)
×
193

194
    return parameters
×
195

196

197
def _prepare_task_data(
1✔
198
    source_url: str,
199
    base_url: str,
200
    parameters: ParametersDict,
201
    task_type: TaskType,
202
) -> RunnerManagerTaskPayload:
203
    """Prepare task payload for runner manager.
204

205
    Args:
206
        source_url: URL to the source file (video or XML)
207
        base_url: Base URL of the site
208
        parameters: Encoding parameters
209

210
    Returns:
211
        Dictionary with task data
212
    """
213
    return {
×
214
        "etab_name": f"{__TITLE_ETB__} / {__TITLE_SITE__}",
215
        "app_name": "Esup-Pod",
216
        "app_version": VERSION,
217
        "task_type": task_type,
218
        "source_url": source_url,
219
        "notify_url": f"{base_url}/runner/notify_task_end/",
220
        "parameters": parameters,
221
    }
222

223

224
# ---- Runner manager helpers (module-level to keep complexity low) ----
225
def _rotate_same_priority_runner_managers(
1✔
226
    runner_managers: list[RunnerManager],
227
) -> list[RunnerManager]:
228
    """Rotate a same-priority runner manager list using last assigned task."""
229
    if len(runner_managers) <= 1:
1✔
230
        return runner_managers
1✔
231

232
    runner_manager_ids = [rm.id for rm in runner_managers]
1✔
233
    last_runner_manager_id = (
1✔
234
        Task.objects.filter(runner_manager_id__in=runner_manager_ids)
235
        .order_by("-date_added", "-id")
236
        .values_list("runner_manager_id", flat=True)
237
        .first()
238
    )
239
    if not last_runner_manager_id or last_runner_manager_id not in runner_manager_ids:
1✔
240
        return runner_managers
1✔
241

242
    last_index = runner_manager_ids.index(last_runner_manager_id)
1✔
243
    return runner_managers[last_index + 1 :] + runner_managers[: last_index + 1]
1✔
244

245

246
def _get_runner_managers(site: Site) -> list[RunnerManager]:
1✔
247
    """Return active site runner managers ordered by priority with round-robin per priority."""
248
    ordered_runner_managers = list(
1✔
249
        RunnerManager.objects.filter(site=site, is_active=True).order_by(
250
            "priority", "id"
251
        )
252
    )
253
    if len(ordered_runner_managers) <= 1:
1✔
254
        return ordered_runner_managers
1✔
255

256
    runner_managers: list[RunnerManager] = []
1✔
257
    current_priority: Optional[int] = None
1✔
258
    current_group: list[RunnerManager] = []
1✔
259
    # Apply round-robin only inside groups that share the same priority level.
260
    for runner_manager in ordered_runner_managers:
1✔
261
        if current_priority is None or runner_manager.priority == current_priority:
1✔
262
            current_group.append(runner_manager)
1✔
263
            current_priority = runner_manager.priority
1✔
264
            continue
1✔
265

266
        runner_managers.extend(_rotate_same_priority_runner_managers(current_group))
1✔
267
        current_group = [runner_manager]
1✔
268
        current_priority = runner_manager.priority
1✔
269

270
    if current_group:
1✔
271
        runner_managers.extend(_rotate_same_priority_runner_managers(current_group))
1✔
272

273
    return runner_managers
1✔
274

275

276
def _ids_for(
1✔
277
    source_type: SourceType, source_id: Union[int, str]
278
) -> tuple[Optional[int], Optional[int]]:
279
    """Return (video_id, recording_id) tuple based on source type."""
280
    return (int(source_id), None) if source_type == "video" else (None, int(source_id))
×
281

282

283
def _execute_url(rm: RunnerManager) -> str:
1✔
284
    """Build the execute endpoint URL for the given runner manager."""
285
    base = rm.url if rm.url.endswith("/") else rm.url + "/"
×
286
    return base + "task/execute"
×
287

288

289
def _headers(rm: RunnerManager) -> HeadersDict:
1✔
290
    """Build authentication and content headers for the runner manager API."""
291
    return {
×
292
        "Accept": "application/json",
293
        "Content-Type": "application/json",
294
        "Authorization": f"Bearer {rm.token}",
295
    }
296

297

298
def _try_send_to_rm(
1✔
299
    rm: RunnerManager, payload: RunnerManagerTaskPayload
300
) -> Optional[requests.Response]:
301
    """Try to POST the payload to a runner manager; log and return None on failure."""
302
    try:
×
303
        return requests.post(
×
304
            _execute_url(rm), data=json.dumps(payload), headers=_headers(rm), timeout=30
305
        )
306
    except requests.RequestException as exc:
×
307
        log.warning(
×
308
            f"Cannot reach runner manager {rm.name}: {str(exc)}. Trying next one."
309
        )
310
        return None
×
311

312

313
def _prestore_encoding_if_needed(
1✔
314
    *,
315
    task_type: TaskType,
316
    source_type: SourceType,
317
    video_id: Optional[int],
318
    recording_id: Optional[int],
319
    rm: RunnerManager,
320
    data: RunnerManagerTaskPayload,
321
) -> None:
322
    """Run pre-store steps for encoding/studio tasks.
323

324
    Does nothing for transcription tasks.
325
    """
326
    if task_type not in ("encoding", "studio"):
×
327
        return
×
328
    execute_url = _execute_url(rm)
×
329
    if source_type == "video":
×
330
        if video_id is not None:
×
331
            store_before_remote_encoding_video(video_id, execute_url, data)
×
332
        else:
333
            log.warning(
×
334
                "Unexpected None video_id for source_type 'video' while preparing store_before_remote_encoding_video."
335
            )
336
    elif source_type == "recording":
×
337
        if recording_id is not None:
×
338
            store_before_remote_encoding_recording(recording_id, execute_url, data)
×
339
        else:
340
            log.warning(
×
341
                "Unexpected None recording_id for source_type 'recording' while preparing store_before_remote_encoding_recording."
342
            )
343

344

345
def _submit_to_runner_manager(
1✔
346
    rm: RunnerManager,
347
    data: RunnerManagerTaskPayload,
348
    task_type: TaskType,
349
    source_type: SourceType,
350
    video_id: Optional[int],
351
    recording_id: Optional[int],
352
) -> bool:
353
    """Submit payload to one runner manager and handle response and pre-store."""
354
    response = _try_send_to_rm(rm, data)
×
355
    if response is None:
×
356
        return False
×
357
    if response.status_code != 200:
×
358
        log.warning(
×
359
            f"Runner manager {rm.name} returned status code {response.status_code}. Trying next one."
360
        )
361
        return False
×
362
    log.info(
×
363
        f"Runner manager {rm.name} is available to process {task_type} for {source_type} {video_id or recording_id}."
364
    )
365
    # Runner may reply with no body; keep an empty payload in that case.
366
    payload = cast(RunnerManagerResponse, response.json() if response.content else {})
×
367
    _update_task_from_response(video_id, recording_id, task_type, rm, payload)
×
368
    _prestore_encoding_if_needed(
×
369
        task_type=task_type,
370
        source_type=source_type,
371
        video_id=video_id,
372
        recording_id=recording_id,
373
        rm=rm,
374
        data=data,
375
    )
376
    return True
×
377

378

379
def _update_task_pending(
1✔
380
    source_type: SourceType, source_id: Union[int, str], task_type: TaskType
381
) -> tuple[Optional[int], Optional[int]]:
382
    """Create or set a pending task for the given source and return (video_id, recording_id)."""
383
    video_id, recording_id = _ids_for(source_type, source_id)
×
384
    log.info(
×
385
        "Update task to pending for video_id: %s, recording_id: %s",
386
        video_id,
387
        recording_id,
388
    )
389
    _edit_task(
×
390
        video_id=video_id,
391
        recording_id=recording_id,
392
        type=task_type,
393
        status="pending",
394
        runner_manager_id=None,
395
        task_id=None,
396
    )
397
    return video_id, recording_id
×
398

399

400
def _update_task_from_response(
1✔
401
    video_id: Optional[int],
402
    recording_id: Optional[int],
403
    task_type: TaskType,
404
    rm: RunnerManager,
405
    response_json: RunnerManagerResponse,
406
) -> None:
407
    """Update the task row using the response payload returned by the runner manager."""
408
    task_id = response_json.get("task_id")
×
409
    status = str(response_json.get("status", "pending"))
×
410
    log.info(
×
411
        "Update task for video_id=%s, recording_id=%s, task_type=%s with response_json=%s",
412
        video_id,
413
        recording_id,
414
        task_type,
415
        response_json,
416
    )
417
    _edit_task(
×
418
        video_id=video_id,
419
        recording_id=recording_id,
420
        type=task_type,
421
        status=status,
422
        runner_manager_id=rm.id,
423
        task_id=task_id,
424
    )
425

426

427
def _send_task_to_runner_manager(
1✔
428
    *,
429
    task_type: TaskType,
430
    source_id: Union[int, str],
431
    source_type: SourceType,
432
    source_url: str,
433
    base_url: str,
434
    parameters: ParametersDict,
435
) -> bool:
436
    """Submit a task to the Runner Manager and update the DB task row.
437

438
    - task_type: one of "encoding", "studio", "transcription"
439
    - source_type: "video" or "recording" (used to resolve ids and pre-store behavior)
440
    """
441

442
    try:
×
443
        # Keep a local pending row even when no runner is currently available.
444
        # This allows process_tasks to retry submission later.
NEW
445
        video_id, recording_id = _update_task_pending(source_type, source_id, task_type)
×
446

447
        site = Site.objects.get_current()
×
448
        runner_managers_list = _get_runner_managers(site)
×
449
        if not runner_managers_list:
×
NEW
450
            log.warning(
×
451
                f"No active runner manager defined for site {site.domain}. Cannot process {task_type} for {source_type} {source_id}."
452
            )
453
            return False
×
454

455
        # Build payload and try immediate submission
456
        data = _prepare_task_data(source_url, base_url, parameters, task_type)
×
457

458
        # Try each runner manager by priority and stop on the first healthy one.
459
        for rm in runner_managers_list:
×
460
            if _submit_to_runner_manager(
×
461
                rm,
462
                data=data,
463
                task_type=task_type,
464
                source_type=source_type,
465
                video_id=video_id,
466
                recording_id=recording_id,
467
            ):
468
                return True
×
469

470
        log.warning(
×
471
            f"No runner manager available to process {task_type} for {source_type} {source_id}. "
472
            f"Task will remain pending and will be retried by the process_tasks command."
473
        )
474
        return False
×
475

476
    except Exception as exc:
×
477
        log.error(
×
478
            f"Error to process {task_type} for {source_type} {source_id}: {str(exc)}"
479
        )
480
        return False
×
481

482

483
def encode_video(video_id: int) -> None:
1✔
484
    """Start video encoding with runner manager."""
485
    log.info("Start encoding, with runner manager, for id: %s" % video_id)
×
486
    try:
×
487
        site = Site.objects.get_current()
×
488
        # Get video info
489
        video = get_object_or_404(Video, id=video_id)
×
490
        # Build content URL
491
        url_scheme = "https" if SECURE_SSL_REDIRECT else "http"
×
492
        base_url = url_scheme + "://" + site.domain
×
493
        content_url = "%s/media/%s" % (base_url, video.video)
×
494

495
        # Prepare encoding parameters
496
        parameters = _prepare_encoding_parameters(video=video)
×
497

498
        # Send encoding task to runner manager
499
        _send_task_to_runner_manager(
×
500
            task_type="encoding",
501
            source_id=video_id,
502
            source_type="video",
503
            source_url=content_url,
504
            base_url=base_url,
505
            parameters=parameters,
506
        )
507

508
    except Exception as exc:
×
509
        log.error(
×
510
            'Error to encode video "%(id)s": %(exc)s'
511
            % {"id": video_id, "exc": str(exc)}
512
        )
513

514

515
def encode_studio_recording(recording_id: int) -> None:
1✔
516
    """Start encoding studio recording with runner manager.
517

518
    This function handles encoding of studio recordings by passing the XML
519
    source file URL to the runner manager.
520
    """
521
    log.info(
×
522
        "Start encoding, with runner manager, for studio recording id %s" % recording_id
523
    )
524
    try:
×
525
        site = Site.objects.get_current()
×
526
        # Get studio recording
527
        recording = Recording.objects.get(id=recording_id)
×
528
        # Source file corresponds to Pod XML file
529
        source_file = recording.source_file
×
530

531
        # Build source file URL.
532
        # `source_file` is sometimes an absolute MEDIA_ROOT path and sometimes already relative.
533
        url_scheme = "https" if SECURE_SSL_REDIRECT else "http"
×
534
        base_url = url_scheme + "://" + site.domain
×
535
        media_url = getattr(settings, "MEDIA_URL", "/media/").rstrip("/")
×
536
        try:
×
537
            rel_path = os.path.relpath(
×
538
                str(source_file), str(getattr(settings, "MEDIA_ROOT", ""))
539
            )
540
        except Exception:
×
541
            rel_path = str(source_file)
×
542
        rel_path = rel_path.lstrip("/")
×
543
        source_url = f"{base_url}{media_url}/{rel_path}"
×
544

545
        # Prepare encoding parameters (no specific cut info for studio recordings)
546
        parameters = _prepare_encoding_parameters(video=None)
×
547

548
        # Send studio task to runner manager
549
        _send_task_to_runner_manager(
×
550
            task_type="studio",
551
            source_id=recording_id,
552
            source_type="recording",
553
            source_url=source_url,
554
            base_url=base_url,
555
            parameters=parameters,
556
        )
557

558
    except Recording.DoesNotExist:
×
559
        log.error(f"Recording {recording_id} not found.")
×
560
    except Exception as exc:
×
561
        log.error(f"Error to encode recording {recording_id}: {str(exc)}")
×
562

563

564
def transcript_video(video_id: int) -> None:
1✔
565
    """Start video transcription with runner manager."""
566
    log.info("Start transcription, with runner manager, for id: %s" % video_id)
×
567
    try:
×
568
        site = Site.objects.get_current()
×
569
        # Get video info
570
        video = get_object_or_404(Video, id=video_id)
×
571
        # Get associated mp3 file if exists
572
        mp3file = video.get_video_mp3().source_file if video.get_video_mp3() else None
×
573
        url_scheme = "https" if SECURE_SSL_REDIRECT else "http"
×
574
        base_url = url_scheme + "://" + site.domain
×
575
        if mp3file is not None:
×
576
            content_url = "%s%s" % (base_url, mp3file.url)
×
577
        else:
578
            # Build video content URL
579
            content_url = "%s/media/%s" % (base_url, video.video)
×
580

581
        # Prepare transcript parameters
582
        parameters = _prepare_transcription_parameters(video=video)
×
583

584
        # Mark video as encoding in progress
585
        video_to_encode = Video.objects.get(id=video_id)
×
586
        video_to_encode.encoding_in_progress = True
×
587
        video_to_encode.save()
×
588

589
        # Update encoding step to transcripting audio
590
        change_encoding_step(video_id, 5, "transcripting audio")
×
591

592
        # Send transcription task to runner manager
593
        _send_task_to_runner_manager(
×
594
            task_type="transcription",
595
            source_id=video_id,
596
            source_type="video",
597
            source_url=content_url,
598
            base_url=base_url,
599
            parameters=parameters,
600
        )
601

602
    except Exception as exc:
×
603
        log.error(
×
604
            'Error to transcribe video "%(id)s": %(exc)s'
605
            % {"id": video_id, "exc": str(exc)}
606
        )
607

608

609
def _prepare_transcription_parameters(video: Video) -> ParametersDict:
1✔
610
    """Prepare parameters for a transcription task.
611

612
    Args:
613
        video: `Video` instance to transcribe.
614

615
    Returns:
616
        Parameter dictionary for the Runner Manager.
617
    """
618
    try:
1✔
619
        from .transcript import resolve_transcription_language
1✔
620

621
        # Requested language (video `transcript` field)
622
        lang = resolve_transcription_language(video)
1✔
623

624
        # Options from settings (optional on runner side)
625
        transcription_type = getattr(settings, "TRANSCRIPTION_TYPE", None)
1✔
626
        normalize = bool(getattr(settings, "TRANSCRIPTION_NORMALIZE", False))
1✔
627

628
        params: ParametersDict = {
1✔
629
            "language": lang,
630
            # Duration may help runner to tune/optimize
631
            "duration": float(getattr(video, "duration", 0) or 0),
632
            # Text normalization (punctuation/casing) on runner side if supported
633
            "normalize": normalize,
634
        }
635
        # If needed in future, we can add model size or other options here
636
        if transcription_type:
1✔
637
            params["model_type"] = transcription_type
×
638
            # Possibility to add model size if needed in future
639
            # params["model"] = "medium"
640

641
        return params
1✔
642
    except Exception:
×
643
        # Keep legacy key name for backward compatibility with older runners.
644
        return {"lang": getattr(video, "transcript", "") or ""}
×
645

646

647
def _edit_task(
1✔
648
    video_id: Optional[int],
649
    type: str,
650
    status: str,
651
    runner_manager_id: Optional[int] = None,
652
    task_id: Optional[str] = None,
653
    recording_id: Optional[int] = None,
654
) -> None:
655
    """Edit or create a task for a video or studio recording."""
656
    try:
×
657
        from .task_queue import refresh_pending_task_ranks
×
658

659
        log.info(
×
660
            f"Edit or create a task: {video_id} {type} {runner_manager_id} {status} {task_id}"
661
        )
662
        # Check if a task already exists for this video and type with pending status
663
        # Build base queryset depending on source type
664
        if type == "studio":
×
665
            tasks_list = list(
×
666
                Task.objects.filter(
667
                    recording_id=recording_id,
668
                    type=type,
669
                    status="pending",
670
                )
671
            )
672
        else:
673
            tasks_list = list(
×
674
                Task.objects.filter(
675
                    video_id=video_id,
676
                    type=type,
677
                    status="pending",
678
                )
679
            )
680
        if not tasks_list:
×
681
            # Create new task
682
            task = Task(
×
683
                video_id=video_id if type != "studio" else None,
684
                recording_id=recording_id if type == "studio" else None,
685
                type=type,
686
                runner_manager_id=runner_manager_id,
687
                status=status,
688
                task_id=task_id,
689
            )
690
            task.save()
×
691
        else:
692
            # Edit existing task
693
            task = tasks_list[0]
×
694
            task.status = status
×
695
            if runner_manager_id is not None:
×
696
                task.runner_manager_id = runner_manager_id
×
697
            if task_id is not None:
×
698
                task.task_id = task_id
×
699
            # Keep association fields as-is
700
            task.save()
×
701

702
        refresh_pending_task_ranks()
×
703

704
    except Exception as exc:
×
705
        log.error(
×
706
            f"Unable to edit a task (video_id={video_id}, recording_id={recording_id}): {str(exc)}"
707
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc