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

EsupPortail / Esup-Pod / 22725050893

05 Mar 2026 03:28PM UTC coverage: 68.07% (+0.003%) from 68.067%
22725050893

Pull #1409

github

web-flow
Merge 7c29945ad into 3b7eb8722
Pull Request #1409: Fix Thumbnail Persistence and Completion Alert

1 of 24 new or added lines in 3 files covered. (4.17%)

8 existing lines in 4 files now uncovered.

12923 of 18985 relevant lines covered (68.07%)

0.68 hits per line

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

35.09
/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("priority", "id")
250
    )
251
    if len(ordered_runner_managers) <= 1:
1✔
252
        return ordered_runner_managers
1✔
253

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

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

268
    if current_group:
1✔
269
        runner_managers.extend(_rotate_same_priority_runner_managers(current_group))
1✔
270

271
    return runner_managers
1✔
272

273

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

280

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

286

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

295

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

310

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

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

342

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

376

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

397

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

424

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

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

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

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

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

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

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

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

480

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

493
        # Prepare encoding parameters
494
        parameters = _prepare_encoding_parameters(video=video)
×
495

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

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

511

512
def encode_studio_recording(recording_id: int) -> None:
1✔
513
    """Start encoding studio recording with runner manager.
514

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

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

542
        # Prepare encoding parameters (no specific cut info for studio recordings)
543
        parameters = _prepare_encoding_parameters(video=None)
×
544

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

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

560

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

578
        # Prepare transcript parameters
579
        parameters = _prepare_transcription_parameters(video=video)
×
580

581
        # Mark video as encoding in progress
NEW
582
        Video.objects.filter(id=video_id).update(encoding_in_progress=True)
×
583

584
        # Update encoding step to transcripting audio
585
        change_encoding_step(video_id, 5, "transcripting audio")
×
586

587
        # Send transcription task to runner manager
588
        _send_task_to_runner_manager(
×
589
            task_type="transcription",
590
            source_id=video_id,
591
            source_type="video",
592
            source_url=content_url,
593
            base_url=base_url,
594
            parameters=parameters,
595
        )
596

597
    except Exception as exc:
×
598
        log.error(
×
599
            'Error to transcribe video "%(id)s": %(exc)s'
600
            % {"id": video_id, "exc": str(exc)}
601
        )
602

603

604
def _prepare_transcription_parameters(video: Video) -> ParametersDict:
1✔
605
    """Prepare parameters for a transcription task.
606

607
    Args:
608
        video: `Video` instance to transcribe.
609

610
    Returns:
611
        Parameter dictionary for the Runner Manager.
612
    """
613
    try:
1✔
614
        from .transcript import resolve_transcription_language
1✔
615

616
        # Requested language (video `transcript` field)
617
        lang = resolve_transcription_language(video)
1✔
618

619
        # Options from settings (optional on runner side)
620
        transcription_type = getattr(settings, "TRANSCRIPTION_TYPE", None)
1✔
621
        normalize = bool(getattr(settings, "TRANSCRIPTION_NORMALIZE", False))
1✔
622

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

636
        return params
1✔
637
    except Exception:
×
638
        # Keep legacy key name for backward compatibility with older runners.
639
        return {"lang": getattr(video, "transcript", "") or ""}
×
640

641

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

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

697
        refresh_pending_task_ranks()
×
698

699
    except Exception as exc:
×
700
        log.error(
×
701
            f"Unable to edit a task (video_id={video_id}, recording_id={recording_id}): {str(exc)}"
702
        )
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