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

EsupPortail / Esup-Pod / 25740886384

12 May 2026 02:25PM UTC coverage: 69.122%. First build
25740886384

Pull #1427

github

web-flow
Bump urllib3 from 2.6.3 to 2.7.0 (#1444)

Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #1427: [Release] 4.3.0

173 of 199 new or added lines in 6 files covered. (86.93%)

13203 of 19101 relevant lines covered (69.12%)

0.69 hits per line

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

99.03
/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
1✔
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()
1✔
101
    str_resolution: dict[str, dict[str, Any]] = {
1✔
102
        str(k): {
103
            "resolution": v["resolution"],
104
            "video_bitrate": v["video_bitrate"],
105
            "audio_bitrate": v["audio_bitrate"],
106
            "encode_mp4": v["encode_mp4"],
107
        }
108
        for k, v in list_rendition.items()
109
    }
110
    return {"rendition": json.dumps(str_resolution)}
1✔
111

112

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

126

127
def _build_base_url(site: Site) -> str:
1✔
128
    """Build the public base URL for a given Django site."""
129
    url_scheme = "https" if SECURE_SSL_REDIRECT else "http"
1✔
130
    return f"{url_scheme}://{site.domain}"
1✔
131

132

133
def _build_video_source_url(video: Video, base_url: str) -> str:
1✔
134
    """Build the public source URL for a video file."""
135
    return f"{base_url}/media/{video.video}"
1✔
136

137

138
def _build_transcription_source_url(video: Video, base_url: str) -> str:
1✔
139
    """Build the preferred source URL for transcription tasks."""
140
    mp3 = video.get_video_mp3() if hasattr(video, "get_video_mp3") else None
1✔
141
    mp3file = getattr(mp3, "source_file", None) if mp3 else None
1✔
142
    if mp3file is not None and getattr(mp3file, "url", None):
1✔
143
        return f"{base_url}{mp3file.url}"
1✔
144
    return _build_video_source_url(video, base_url)
1✔
145

146

147
def _build_studio_source_url(recording: Recording, base_url: str) -> str:
1✔
148
    """Build the public source URL for a studio XML file."""
149
    source_file = recording.source_file
1✔
150
    media_url = getattr(settings, "MEDIA_URL", "/media/").rstrip("/")
1✔
151
    try:
1✔
152
        rel_path = os.path.relpath(
1✔
153
            str(source_file), str(getattr(settings, "MEDIA_ROOT", ""))
154
        )
155
    except Exception:
1✔
156
        rel_path = str(source_file)
1✔
157
    rel_path = rel_path.lstrip("/")
1✔
158
    return f"{base_url}{media_url}/{rel_path}"
1✔
159

160

161
def _build_media_asset_url(base_url: str, asset_name: str) -> str:
1✔
162
    """Build a public media URL for a stored asset name."""
163
    return f"{base_url}/media/{asset_name}"
1✔
164

165

166
def _extend_with_watermark_info(
1✔
167
    dressing_info: ParametersDict, dressing: Any, base_url: str
168
) -> None:
169
    """Append watermark-related dressing metadata when available."""
170
    if not dressing.watermark:
1✔
NEW
171
        return
×
172

173
    log.info("Dressing watermark found")
1✔
174
    dressing_info["watermark"] = _build_media_asset_url(
1✔
175
        base_url, str(dressing.watermark.file.name)
176
    )
177
    dressing_info["watermark_position"] = dressing.position
1✔
178
    dressing_info["watermark_opacity"] = str(dressing.opacity)
1✔
179

180

181
def _extend_with_credit_info(
1✔
182
    dressing_info: ParametersDict, credit_name: str, credit, base_url: str
183
) -> None:
184
    """Append opening/ending credit metadata when available."""
185
    if not credit:
1✔
NEW
186
        return
×
187

188
    log.info("Dressing %s found", credit_name.replace("_", " "))
1✔
189
    dressing_info[credit_name] = credit.slug
1✔
190
    dressing_info[f"{credit_name}_video"] = _build_media_asset_url(
1✔
191
        base_url, str(credit.video.name)
192
    )
193
    dressing_info[f"{credit_name}_video_duration"] = str(credit.duration)
1✔
194

195

196
def _attach_dressing_info(
1✔
197
    parameters: ParametersDict, video: Video, base_url: Optional[str] = None
198
) -> None:
199
    """Attach dressing information to parameters if available for the given video."""
200
    try:
1✔
201
        from pod.dressing.models import Dressing
1✔
202

203
        if base_url is None:
1✔
204
            site = Site.objects.get_current()
1✔
205
            base_url = _build_base_url(site)
1✔
206

207
        if not Dressing.objects.filter(videos=video).exists():
1✔
208
            return
1✔
209

210
        log.info("Dressing found for video id: %s", video.id)
1✔
211
        dressing = Dressing.objects.get(videos=video)
1✔
212
        if not dressing:
1✔
NEW
213
            return
×
214

215
        dressing_info: ParametersDict = {}
1✔
216
        _extend_with_watermark_info(dressing_info, dressing, base_url)
1✔
217
        _extend_with_credit_info(
1✔
218
            dressing_info, "opening_credits", dressing.opening_credits, base_url
219
        )
220
        _extend_with_credit_info(
1✔
221
            dressing_info, "ending_credits", dressing.ending_credits, base_url
222
        )
223

224
        if dressing_info:
1✔
225
            parameters["dressing"] = json.dumps(dressing_info)
1✔
226
    except Exception as exc:
1✔
227
        log.error(f"Error obtaining dressing for video {video.id}: {str(exc)}")
1✔
228

229

230
def _attach_video_metadata(parameters: ParametersDict, video: Video) -> None:
1✔
231
    """Attach lightweight video metadata when available."""
232
    video_id = getattr(video, "id", None)
1✔
233
    if video_id is not None:
1✔
234
        parameters["video_id"] = int(video_id)
1✔
235

236
    video_slug = getattr(video, "slug", None)
1✔
237
    if video_slug:
1✔
238
        parameters["video_slug"] = str(video_slug)
1✔
239

240
    video_title = getattr(video, "title", None)
1✔
241
    if video_title:
1✔
242
        parameters["video_title"] = str(video_title)
1✔
243

244

245
def _prepare_encoding_parameters(
1✔
246
    video: Optional[Video] = None,
247
    base_url: Optional[str] = None,
248
) -> ParametersDict:
249
    """Prepare encoding parameters for video or recording.
250

251
    Args:
252
        video: Video object (for video encoding).
253
               For studio recordings, pass None as video-specific metadata,
254
               cut and dressing info do not apply.
255
        base_url: Explicit base URL to use for dressing asset URLs.
256
                  Defaults to the current site URL when omitted.
257

258
    Returns:
259
        Dictionary with rendition and, when a video is provided, optional
260
        cut info, optional dressing info, and video metadata
261
        (video_id, video_slug, video_title).
262
    """
263
    parameters = _build_rendition_parameters()
1✔
264

265
    if video:
1✔
266
        _attach_cut_info(parameters, video)
1✔
267
        if base_url is None:
1✔
268
            _attach_dressing_info(parameters, video)
1✔
269
        else:
270
            _attach_dressing_info(parameters, video, base_url=base_url)
1✔
271
        _attach_video_metadata(parameters, video)
1✔
272

273
    return parameters
1✔
274

275

276
def _prepare_task_data(
1✔
277
    source_url: str,
278
    base_url: str,
279
    parameters: ParametersDict,
280
    task_type: TaskType,
281
) -> RunnerManagerTaskPayload:
282
    """Prepare task payload for runner manager.
283

284
    Args:
285
        source_url: URL to the source file (video or XML)
286
        base_url: Base URL of the site
287
        parameters: Encoding parameters
288

289
    Returns:
290
        Dictionary with task data
291
    """
292
    return {
1✔
293
        "etab_name": f"{__TITLE_ETB__} / {__TITLE_SITE__}",
294
        "app_name": "Esup-Pod",
295
        "app_version": VERSION,
296
        "task_type": task_type,
297
        "source_url": source_url,
298
        "notify_url": f"{base_url}/runner/notify_task_end/",
299
        "parameters": parameters,
300
    }
301

302

303
# ---- Runner manager helpers (module-level to keep complexity low) ----
304
def _rotate_same_priority_runner_managers(
1✔
305
    runner_managers: list[RunnerManager],
306
) -> list[RunnerManager]:
307
    """Rotate a same-priority runner manager list using last assigned task."""
308
    if len(runner_managers) <= 1:
1✔
309
        return runner_managers
1✔
310

311
    runner_manager_ids = [rm.id for rm in runner_managers]
1✔
312
    last_runner_manager_id = (
1✔
313
        Task.objects.filter(runner_manager_id__in=runner_manager_ids)
314
        .order_by("-date_added", "-id")
315
        .values_list("runner_manager_id", flat=True)
316
        .first()
317
    )
318
    if not last_runner_manager_id or last_runner_manager_id not in runner_manager_ids:
1✔
319
        return runner_managers
1✔
320

321
    last_index = runner_manager_ids.index(last_runner_manager_id)
1✔
322
    return runner_managers[last_index + 1 :] + runner_managers[: last_index + 1]
1✔
323

324

325
def _get_runner_managers(site: Site) -> list[RunnerManager]:
1✔
326
    """Return active site runner managers ordered by priority with round-robin per priority."""
327
    ordered_runner_managers = list(
1✔
328
        RunnerManager.objects.filter(site=site, is_active=True).order_by("priority", "id")
329
    )
330
    if len(ordered_runner_managers) <= 1:
1✔
331
        return ordered_runner_managers
1✔
332

333
    runner_managers: list[RunnerManager] = []
1✔
334
    current_priority: Optional[int] = None
1✔
335
    current_group: list[RunnerManager] = []
1✔
336
    # Apply round-robin only inside groups that share the same priority level.
337
    for runner_manager in ordered_runner_managers:
1✔
338
        if current_priority is None or runner_manager.priority == current_priority:
1✔
339
            current_group.append(runner_manager)
1✔
340
            current_priority = runner_manager.priority
1✔
341
            continue
1✔
342

343
        runner_managers.extend(_rotate_same_priority_runner_managers(current_group))
1✔
344
        current_group = [runner_manager]
1✔
345
        current_priority = runner_manager.priority
1✔
346

347
    if current_group:
1✔
348
        runner_managers.extend(_rotate_same_priority_runner_managers(current_group))
1✔
349

350
    return runner_managers
1✔
351

352

353
def _ids_for(
1✔
354
    source_type: SourceType, source_id: Union[int, str]
355
) -> tuple[Optional[int], Optional[int]]:
356
    """Return (video_id, recording_id) tuple based on source type."""
357
    return (int(source_id), None) if source_type == "video" else (None, int(source_id))
1✔
358

359

360
def _execute_url(rm: RunnerManager) -> str:
1✔
361
    """Build the execute endpoint URL for the given runner manager."""
362
    base = rm.url if rm.url.endswith("/") else rm.url + "/"
1✔
363
    return base + "task/execute"
1✔
364

365

366
def _headers(rm: RunnerManager) -> HeadersDict:
1✔
367
    """Build authentication and content headers for the runner manager API."""
368
    return {
1✔
369
        "Accept": "application/json",
370
        "Content-Type": "application/json",
371
        "Authorization": f"Bearer {rm.token}",
372
    }
373

374

375
def _try_send_to_rm(
1✔
376
    rm: RunnerManager, payload: RunnerManagerTaskPayload
377
) -> Optional[requests.Response]:
378
    """Try to POST the payload to a runner manager; log and return None on failure."""
379
    try:
1✔
380
        return requests.post(
1✔
381
            _execute_url(rm), data=json.dumps(payload), headers=_headers(rm), timeout=30
382
        )
383
    except requests.RequestException as exc:
1✔
384
        log.warning(
1✔
385
            f"Cannot reach runner manager {rm.name}: {str(exc)}. Trying next one."
386
        )
387
        return None
1✔
388

389

390
def _parse_runner_response(
1✔
391
    rm: RunnerManager, response: requests.Response
392
) -> Optional[RunnerManagerResponse]:
393
    """Parse and validate a runner manager HTTP response.
394

395
    Returns None when the response body is not a valid JSON payload expected from
396
    the runner manager (for example an HTML error page returned by a reverse proxy
397
    with HTTP 200).
398
    """
399
    if not response.content:
1✔
400
        return {}
1✔
401

402
    try:
1✔
403
        payload = response.json()
1✔
404
    except ValueError:
1✔
405
        content_type = response.headers.get("Content-Type", "")
1✔
406
        log.warning(
1✔
407
            "Runner manager %s returned HTTP 200 with non-JSON body "
408
            "(Content-Type=%s). Trying next one.",
409
            rm.name,
410
            content_type or "unknown",
411
        )
412
        return None
1✔
413

414
    if not isinstance(payload, dict):
1✔
415
        log.warning(
1✔
416
            "Runner manager %s returned an unexpected JSON payload type (%s). "
417
            "Trying next one.",
418
            rm.name,
419
            type(payload).__name__,
420
        )
421
        return None
1✔
422

423
    return cast(RunnerManagerResponse, payload)
1✔
424

425

426
def _prestore_encoding_if_needed(
1✔
427
    *,
428
    task_type: TaskType,
429
    source_type: SourceType,
430
    video_id: Optional[int],
431
    recording_id: Optional[int],
432
    rm: RunnerManager,
433
    data: RunnerManagerTaskPayload,
434
) -> None:
435
    """Run pre-store steps for encoding/studio tasks.
436

437
    Does nothing for transcription tasks.
438
    """
439
    if task_type not in ("encoding", "studio"):
1✔
440
        return
1✔
441
    execute_url = _execute_url(rm)
1✔
442
    if source_type == "video":
1✔
443
        if video_id is not None:
1✔
444
            store_before_remote_encoding_video(video_id, execute_url, data)
1✔
445
        else:
446
            log.warning(
1✔
447
                "Unexpected None video_id for source_type 'video' while preparing store_before_remote_encoding_video."
448
            )
449
    elif source_type == "recording":
1✔
450
        if recording_id is not None:
1✔
451
            store_before_remote_encoding_recording(recording_id, execute_url, data)
1✔
452
        else:
453
            log.warning(
1✔
454
                "Unexpected None recording_id for source_type 'recording' while preparing store_before_remote_encoding_recording."
455
            )
456

457

458
def _submit_to_runner_manager(
1✔
459
    rm: RunnerManager,
460
    data: RunnerManagerTaskPayload,
461
    task_type: TaskType,
462
    source_type: SourceType,
463
    video_id: Optional[int],
464
    recording_id: Optional[int],
465
) -> bool:
466
    """Submit payload to one runner manager and handle response and pre-store."""
467
    response = _try_send_to_rm(rm, data)
1✔
468
    if response is None:
1✔
469
        return False
1✔
470
    if response.status_code != 200:
1✔
471
        log.warning(
1✔
472
            f"Runner manager {rm.name} returned status code {response.status_code}. Trying next one."
473
        )
474
        return False
1✔
475
    log.info(
1✔
476
        f"Runner manager {rm.name} is available to process {task_type} for {source_type} {video_id or recording_id}."
477
    )
478
    payload = _parse_runner_response(rm, response)
1✔
479
    if payload is None:
1✔
480
        return False
1✔
481

482
    _update_task_from_response(video_id, recording_id, task_type, rm, payload)
1✔
483
    _prestore_encoding_if_needed(
1✔
484
        task_type=task_type,
485
        source_type=source_type,
486
        video_id=video_id,
487
        recording_id=recording_id,
488
        rm=rm,
489
        data=data,
490
    )
491
    return True
1✔
492

493

494
def _submit_to_runner_managers(
1✔
495
    *,
496
    runner_managers: list[RunnerManager],
497
    data: RunnerManagerTaskPayload,
498
    task_type: TaskType,
499
    source_type: SourceType,
500
    source_id: Union[int, str],
501
) -> bool:
502
    """Try runner managers in order and stop on the first successful submission."""
503
    video_id, recording_id = _ids_for(source_type, source_id)
1✔
504

505
    for rm in runner_managers:
1✔
506
        if _submit_to_runner_manager(
1✔
507
            rm,
508
            data=data,
509
            task_type=task_type,
510
            source_type=source_type,
511
            video_id=video_id,
512
            recording_id=recording_id,
513
        ):
514
            return True
1✔
515

516
    log.warning(
1✔
517
        f"No runner manager available to process {task_type} for {source_type} {source_id}. "
518
        f"Task will remain pending and will be retried by the process_tasks command."
519
    )
520
    return False
1✔
521

522

523
def _update_task_pending(
1✔
524
    source_type: SourceType, source_id: Union[int, str], task_type: TaskType
525
) -> tuple[Optional[int], Optional[int]]:
526
    """Create or set a pending task for the given source and return (video_id, recording_id)."""
527
    video_id, recording_id = _ids_for(source_type, source_id)
1✔
528
    log.info(
1✔
529
        "Update task to pending for video_id: %s, recording_id: %s",
530
        video_id,
531
        recording_id,
532
    )
533
    _edit_task(
1✔
534
        video_id=video_id,
535
        recording_id=recording_id,
536
        type=task_type,
537
        status="pending",
538
        runner_manager_id=None,
539
        task_id=None,
540
    )
541
    return video_id, recording_id
1✔
542

543

544
def _update_task_from_response(
1✔
545
    video_id: Optional[int],
546
    recording_id: Optional[int],
547
    task_type: TaskType,
548
    rm: RunnerManager,
549
    response_json: RunnerManagerResponse,
550
) -> None:
551
    """Update the task row using the response payload returned by the runner manager."""
552
    task_id = response_json.get("task_id")
1✔
553
    status = str(response_json.get("status", "pending"))
1✔
554
    log.info(
1✔
555
        "Update task for video_id=%s, recording_id=%s, task_type=%s with response_json=%s",
556
        video_id,
557
        recording_id,
558
        task_type,
559
        response_json,
560
    )
561
    _edit_task(
1✔
562
        video_id=video_id,
563
        recording_id=recording_id,
564
        type=task_type,
565
        status=status,
566
        runner_manager_id=rm.id,
567
        task_id=task_id,
568
    )
569

570

571
def _send_task_to_runner_manager(
1✔
572
    *,
573
    task_type: TaskType,
574
    source_id: Union[int, str],
575
    source_type: SourceType,
576
    source_url: str,
577
    base_url: str,
578
    parameters: ParametersDict,
579
) -> bool:
580
    """Submit a task to the Runner Manager and update the DB task row.
581

582
    - task_type: one of "encoding", "studio", "transcription"
583
    - source_type: "video" or "recording" (used to resolve ids and pre-store behavior)
584
    """
585

586
    try:
1✔
587
        # Keep a local pending row even when no runner is currently available.
588
        # This allows process_tasks to retry submission later.
589
        _update_task_pending(source_type, source_id, task_type)
1✔
590

591
        site = Site.objects.get_current()
1✔
592
        runner_managers_list = _get_runner_managers(site)
1✔
593
        if not runner_managers_list:
1✔
594
            log.warning(
1✔
595
                f"No active runner manager defined for site {site.domain}. Cannot process {task_type} for {source_type} {source_id}."
596
            )
597
            return False
1✔
598

599
        # Build payload and try immediate submission
600
        data = _prepare_task_data(source_url, base_url, parameters, task_type)
1✔
601

602
        return _submit_to_runner_managers(
1✔
603
            runner_managers=runner_managers_list,
604
            data=data,
605
            task_type=task_type,
606
            source_type=source_type,
607
            source_id=source_id,
608
        )
609

610
    except Exception as exc:
1✔
611
        log.error(
1✔
612
            f"Error to process {task_type} for {source_type} {source_id}: {str(exc)}"
613
        )
614
        return False
1✔
615

616

617
def encode_video(video_id: int) -> None:
1✔
618
    """Start video encoding with runner manager."""
619
    log.info("Start encoding, with runner manager, for id: %s" % video_id)
1✔
620
    try:
1✔
621
        site = Site.objects.get_current()
1✔
622
        # Get video info
623
        video = get_object_or_404(Video, id=video_id)
1✔
624
        # Build content URL
625
        base_url = _build_base_url(site)
1✔
626
        content_url = _build_video_source_url(video, base_url)
1✔
627

628
        # Prepare encoding parameters
629
        parameters = _prepare_encoding_parameters(video=video)
1✔
630

631
        # Send encoding task to runner manager
632
        _send_task_to_runner_manager(
1✔
633
            task_type="encoding",
634
            source_id=video_id,
635
            source_type="video",
636
            source_url=content_url,
637
            base_url=base_url,
638
            parameters=parameters,
639
        )
640

641
    except Exception as exc:
1✔
642
        log.error(
1✔
643
            'Error to encode video "%(id)s": %(exc)s' % {"id": video_id, "exc": str(exc)}
644
        )
645

646

647
def encode_studio_recording(recording_id: int) -> None:
1✔
648
    """Start encoding studio recording with runner manager.
649

650
    This function handles encoding of studio recordings by passing the XML
651
    source file URL to the runner manager.
652
    """
653
    log.info(
1✔
654
        "Start encoding, with runner manager, for studio recording id %s" % recording_id
655
    )
656
    try:
1✔
657
        site = Site.objects.get_current()
1✔
658
        # Get studio recording
659
        recording = Recording.objects.get(id=recording_id)
1✔
660
        base_url = _build_base_url(site)
1✔
661
        source_url = _build_studio_source_url(recording, base_url)
1✔
662

663
        # Prepare encoding parameters (no specific cut info for studio recordings)
664
        parameters = _prepare_encoding_parameters(video=None)
1✔
665

666
        # Send studio task to runner manager
667
        _send_task_to_runner_manager(
1✔
668
            task_type="studio",
669
            source_id=recording_id,
670
            source_type="recording",
671
            source_url=source_url,
672
            base_url=base_url,
673
            parameters=parameters,
674
        )
675

676
    except Recording.DoesNotExist:
1✔
677
        log.error(f"Recording {recording_id} not found.")
1✔
678
    except Exception as exc:
1✔
679
        log.error(f"Error to encode recording {recording_id}: {str(exc)}")
1✔
680

681

682
def transcript_video(video_id: int) -> None:
1✔
683
    """Start video transcription with runner manager."""
684
    log.info("Start transcription, with runner manager, for id: %s" % video_id)
1✔
685
    try:
1✔
686
        site = Site.objects.get_current()
1✔
687
        # Get video info
688
        video = get_object_or_404(Video, id=video_id)
1✔
689
        base_url = _build_base_url(site)
1✔
690
        content_url = _build_transcription_source_url(video, base_url)
1✔
691

692
        # Prepare transcript parameters
693
        parameters = _prepare_transcription_parameters(video=video)
1✔
694

695
        # Mark video as encoding in progress
696
        Video.objects.filter(id=video_id).update(encoding_in_progress=True)
1✔
697

698
        # Update encoding step to transcripting audio
699
        change_encoding_step(video_id, 5, "transcripting audio")
1✔
700

701
        # Send transcription task to runner manager
702
        _send_task_to_runner_manager(
1✔
703
            task_type="transcription",
704
            source_id=video_id,
705
            source_type="video",
706
            source_url=content_url,
707
            base_url=base_url,
708
            parameters=parameters,
709
        )
710

711
    except Exception as exc:
1✔
712
        log.error(
1✔
713
            'Error to transcribe video "%(id)s": %(exc)s'
714
            % {"id": video_id, "exc": str(exc)}
715
        )
716

717

718
def _prepare_transcription_parameters(video: Video) -> ParametersDict:
1✔
719
    """Prepare parameters for a transcription task.
720

721
    Args:
722
        video: `Video` instance to transcribe.
723

724
    Returns:
725
        Parameter dictionary for the Runner Manager.
726
    """
727
    try:
1✔
728
        from .transcript import resolve_transcription_language
1✔
729

730
        # Requested language (video `transcript` field)
731
        lang = resolve_transcription_language(video)
1✔
732

733
        # Options from settings (optional on runner side)
734
        transcription_type = getattr(settings, "TRANSCRIPTION_TYPE", None)
1✔
735
        normalize = bool(getattr(settings, "TRANSCRIPTION_NORMALIZE", False))
1✔
736

737
        params: ParametersDict = {
1✔
738
            "language": lang,
739
            # Duration may help runner to tune/optimize
740
            "duration": float(getattr(video, "duration", 0) or 0),
741
            # Text normalization (punctuation/casing) on runner side if supported
742
            "normalize": normalize,
743
        }
744
        _attach_video_metadata(params, video)
1✔
745
        # If needed in future, we can add model size or other options here
746
        if transcription_type:
1✔
747
            params["model_type"] = transcription_type
1✔
748
            # Possibility to add model size if needed in future
749
            # params["model"] = "medium"
750

751
        return params
1✔
752
    except Exception:
1✔
753
        # Keep legacy key name for backward compatibility with older runners.
754
        params: ParametersDict = {"lang": getattr(video, "transcript", "") or ""}
1✔
755
        _attach_video_metadata(params, video)
1✔
756
        return params
1✔
757

758

759
def _edit_task(
1✔
760
    video_id: Optional[int],
761
    type: str,
762
    status: str,
763
    runner_manager_id: Optional[int] = None,
764
    task_id: Optional[str] = None,
765
    recording_id: Optional[int] = None,
766
) -> None:
767
    """Edit or create a task for a video or studio recording."""
768
    try:
1✔
769
        from .task_queue import refresh_pending_task_ranks
1✔
770

771
        log.info(
1✔
772
            f"Edit or create a task: {video_id} {type} {runner_manager_id} {status} {task_id}"
773
        )
774
        # Check if a task already exists for this video and type with pending status
775
        # Build base queryset depending on source type
776
        if type == "studio":
1✔
777
            tasks_list = list(
1✔
778
                Task.objects.filter(
779
                    recording_id=recording_id,
780
                    type=type,
781
                    status="pending",
782
                )
783
            )
784
        else:
785
            tasks_list = list(
1✔
786
                Task.objects.filter(
787
                    video_id=video_id,
788
                    type=type,
789
                    status="pending",
790
                )
791
            )
792
        if not tasks_list:
1✔
793
            # Create new task
794
            task = Task(
1✔
795
                video_id=video_id if type != "studio" else None,
796
                recording_id=recording_id if type == "studio" else None,
797
                type=type,
798
                runner_manager_id=runner_manager_id,
799
                status=status,
800
                task_id=task_id,
801
            )
802
            task.save()
1✔
803
        else:
804
            # Edit existing task
805
            task = tasks_list[0]
1✔
806
            task.status = status
1✔
807
            if runner_manager_id is not None:
1✔
808
                task.runner_manager_id = runner_manager_id
1✔
809
            if task_id is not None:
1✔
810
                task.task_id = task_id
1✔
811
            # Keep association fields as-is
812
            task.save()
1✔
813

814
        refresh_pending_task_ranks()
1✔
815

816
    except Exception as exc:
1✔
817
        log.error(
1✔
818
            f"Unable to edit a task (video_id={video_id}, recording_id={recording_id}): {str(exc)}"
819
        )
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