Skip to content

perf(cloud-ai): analyse each batch concurrently in process_batch_videos - #1188

Merged
groupthinking merged 2 commits into
mainfrom
perf/batch-video-analyze
Aug 1, 2026
Merged

perf(cloud-ai): analyse each batch concurrently in process_batch_videos#1188
groupthinking merged 2 commits into
mainfrom
perf/batch-video-analyze

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1187

Outcome

process_batch_videos now analyses each batch concurrently, so batch_size becomes the real bound on in-flight calls to the shared upstream AI providers instead of a parameter that only set the cadence of a sleep(1).

Before After
Peak concurrent analyze_video per batch 1 batch_size
Wall-clock for a batch of N sum of all N analyses ~slowest single analysis
What batch_size controlled only the pause cadence actual concurrency bound

What this does not change: the number of provider API calls, token spend, or quota consumed — it is strictly a latency change. One request per video, exactly as before.

Scope

  • src/youtube_extension/backend/cloud_ai_routes.py
    • inner sequential for video_url in batch: loop → asyncio.gather(..., return_exceptions=True)
    • loop-local import asyncio hoisted to module scope
  • tests/unit/test_cloud_ai_routes_batch.py (new, 5 tests)

Design notes

Why unbounded gather over the batch is the correct bound here. analyze_video fans out over a shared resource — one CloudAIIntegrator HTTP client and a provider quota — so this genuinely needs a concurrency limit, unlike a WebSocket broadcast where each peer owns an independent send buffer. The limit already exists: it is batch_size, supplied by the caller and already used to slice the work. Gathering within a batch and keeping batches strictly sequential means peak in-flight calls can never exceed batch_size. Adding a second semaphore inside the batch would be redundant and would let the two bounds drift apart. test_batch_size_bounds_concurrency pins this contract.

Why isinstance(result, Exception) — and why that alone was WRONG (corrected at 8725b51d). This note originally claimed the check was exact parity for the except Exception: clause it replaces, on the reasoning that CancelledError derives from BaseException and so would still propagate. That was backwards. asyncio.gather(..., return_exceptions=True) captures a child's CancelledError as a value rather than raising it, so isinstance(result, Exception) returned False and the error object fell through to format_analysis_result — raising an AttributeError that the outer except Exception swallowed, silently abandoning every remaining batch. Caught by @copilot in review. The code now collects BaseException-but-not-Exception results and re-raises the first one before the per-video failure handling, restoring the original propagation semantics; two regression tests cover it. zip(..., strict=True) is retained so a length mismatch is loud.

Ordering. asyncio.gather returns results in argument order, so results is appended in the same order the sequential loop produced.

Risk

Low. Behaviour-preserving apart from concurrency: per-video failure isolation, result ordering, the inter-batch pause, and the log message on failure are all unchanged and pinned by tests. The function is a fire-and-forget BackgroundTasks job, so it is not on any request's critical path.

Verification

At head 757e8475c:

$ pytest tests/unit/test_cloud_ai_routes_batch.py tests/unit/test_cloud_ai_integrator.py \
         tests/unit/test_cloud_ai_config.py tests/unit/test_cloud_ai_exceptions.py -q
152 passed in 0.62s

$ ruff check src/youtube_extension/backend/cloud_ai_routes.py
Found 7 errors.        # identical to origin/main (7 pre-existing B904, outside this hunk)
$ ruff check tests/unit/test_cloud_ai_routes_batch.py
All checks passed!

Non-vacuity — behavioural mutation. Module-level import asyncio kept, only the semantics reverted to the sequential loop:

E  AssertionError: batch peaked at 1 concurrent analyze_video call(s) for a batch of 4
   - the batch is being analysed sequentially
E  AssertionError: batch_size=2 but peak concurrency was 1; the batch boundary must
   still bound in-flight provider calls
2 failed, 3 passed

The other 3 tests pass under both implementations by design — they are regression guards for the behaviour this PR must preserve (failure isolation, pause cadence, empty input).

Production evidence

Dockerfile:93youtube_extension.main:appmain.py:171 imports cloud_ai_routesPOST /api/v1/cloud-ai/analyze/batch (cloud_ai_routes.py:288) → background_tasks.add_task(process_batch_videos, ...) (:289). cloud_ai_routes is in the transitive import closure of the deployed application.

Agent handoff

Reviewed by @coderabbitai (see review request comment). Follow-ups, if any, will be filed as separate issues rather than expanding this PR's scope.

The batch loop sliced video_urls into batch_size chunks but then awaited
ai.analyze_video() for each video in turn, so batch_size controlled nothing
except how often the inter-batch pause fired - wall-clock cost stayed the
full sum of every per-video analysis.

Analyse each batch with asyncio.gather(..., return_exceptions=True) so
batch_size becomes the real bound on concurrent calls to the shared upstream
AI providers. isinstance(result, Exception) preserves the previous
except-Exception-and-continue semantics exactly, so one failing video is
logged and skipped without aborting its batch.

Also hoists the loop-local 'import asyncio' to module scope.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 1, 2026 21:53
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Canceled Canceled Aug 1, 2026 10:03pm

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 594d3576-7bd3-42d1-ba12-10b64a04d55f

📥 Commits

Reviewing files that changed from the base of the PR and between d6a39c1 and 8725b51.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_cloud_ai_routes_batch.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/backend/cloud_ai_routes.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: copilot
  • GitHub Check: Generate and Upload Coverage
  • GitHub Check: trivy
  • GitHub Check: Security Scan - python
  • GitHub Check: Security Scan - javascript
  • GitHub Check: test
⚠️ CI failures not shown inline (17)

GitHub Actions: PR Governance / 0_Canonical issue and evidence.txt: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@v8
 with:
   script: const pr = context.payload.pull_request;
const runUrl =
  `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
async function publish(conclusion, title, summary) {
  await github.rest.checks.create({
    owner: context.repo.owner,
    repo: context.repo.repo,
    name: "PR Governance",
    head_sha: pr.head.sha,
    status: "completed",
    conclusion,
    details_url: runUrl,
    output: {
      title,
      summary: summary.slice(0, 60000)
    }
  });
  if (conclusion === "failure") {
    core.setFailed(summary);
  }
}
if (pr.draft) {
  await publish(
    "neutral",
    "Governance deferred for draft PR",
    `Draft PR #${pr.number} is not enforced. The Check is bound to exact head ${pr.head.sha}.`
  );
  return;
}
const body = pr.body || "";
function getSectionContent(text, heading) {
  const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const pattern = new RegExp(
    escapedHeading + "\\s*\\n([\\s\\S]*?)(?=\\n## |$)",
    "i"
  );
  const match = text.match(pattern);
  if (!match) return null;
  return match[1].replace(/<!--[\s\S]*?-->/g, "").trim();
}
const placeholderPatterns = [
  /^Describe the user or operational result this PR produces\.?$/i,
  /^List exact automated and manual checks, tied to the current head SHA\.?$/i,
  /^Provide the Vercel preview, production deployment, runtime evidence, or state why production evidence is not applicable\.?$/i,
  /^-\s*Risk level:\s*low\s*\/\s*medium\s*\/\s*high\s*$/i,
  /^-\s*Failure mode:\s*$/i,
  /^-\s*Rollback:\s*$/i,
  /^-\s*\[\s\]\s*(Focused tests|Required CI|Review threads resolved)\s*$/i,
  /^(Closes?|Fix(?:es|ed)?|Resolves?)\s+#\s*$/i
];
function hasMeaningfulContent(content) {
  if (content === null) return false;
  const meaningfulLines = content
    .split(/\r?\n/)
    .map(line => line.trim())
    .filter(Boolean)
    .filter(line => !placeholderPatterns.some(pattern => pattern.test...

GitHub Actions: PR Governance / Canonical issue and evidence: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@v8
 with:
   script: const pr = context.payload.pull_request;
const runUrl =
  `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
async function publish(conclusion, title, summary) {
  await github.rest.checks.create({
    owner: context.repo.owner,
    repo: context.repo.repo,
    name: "PR Governance",
    head_sha: pr.head.sha,
    status: "completed",
    conclusion,
    details_url: runUrl,
    output: {
      title,
      summary: summary.slice(0, 60000)
    }
  });
  if (conclusion === "failure") {
    core.setFailed(summary);
  }
}
if (pr.draft) {
  await publish(
    "neutral",
    "Governance deferred for draft PR",
    `Draft PR #${pr.number} is not enforced. The Check is bound to exact head ${pr.head.sha}.`
  );
  return;
}
const body = pr.body || "";
function getSectionContent(text, heading) {
  const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const pattern = new RegExp(
    escapedHeading + "\\s*\\n([\\s\\S]*?)(?=\\n## |$)",
    "i"
  );
  const match = text.match(pattern);
  if (!match) return null;
  return match[1].replace(/<!--[\s\S]*?-->/g, "").trim();
}
const placeholderPatterns = [
  /^Describe the user or operational result this PR produces\.?$/i,
  /^List exact automated and manual checks, tied to the current head SHA\.?$/i,
  /^Provide the Vercel preview, production deployment, runtime evidence, or state why production evidence is not applicable\.?$/i,
  /^-\s*Risk level:\s*low\s*\/\s*medium\s*\/\s*high\s*$/i,
  /^-\s*Failure mode:\s*$/i,
  /^-\s*Rollback:\s*$/i,
  /^-\s*\[\s\]\s*(Focused tests|Required CI|Review threads resolved)\s*$/i,
  /^(Closes?|Fix(?:es|ed)?|Resolves?)\s+#\s*$/i
];
function hasMeaningfulContent(content) {
  if (content === null) return false;
  const meaningfulLines = content
    .split(/\r?\n/)
    .map(line => line.trim())
    .filter(Boolean)
    .filter(line => !placeholderPatterns.some(pattern => pattern.test...

GitHub Actions: Secret Scan / gitleaks (working tree): perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run gitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1
 �[36;1mgitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
     ○
     │╲
     │ ○
     ○ ░
     ░    gitleaks
 Finding:     ...gz", hash = "sha256:�[1;3;mREDACTED�[0m, size = 401824, upl...
 ***REDACTED_SECRET_ASSIGNMENT***
 RuleID:      square-access-token
 Entropy:     3.884400
 File:        uv.lock
 Line:        5129
 Fingerprint: uv.lock:square-access-***REDACTED_SECRET_ASSIGNMENT***
 �[90m10:03PM�[0m �[32mINF�[0m scan completed in 5.9s
 �[90m10:03PM�[0m �[31mWRN�[0m leaks found: 1
 ##[error]Process completed with exit code 1.

GitHub Actions: Secret Scan / 0_gitleaks (working tree).txt: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run gitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1
 �[36;1mgitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
     ○
     │╲
     │ ○
     ○ ░
     ░    gitleaks
 Finding:     ...gz", hash = "sha256:�[1;3;mREDACTED�[0m, size = 401824, upl...
 ***REDACTED_SECRET_ASSIGNMENT***
 RuleID:      square-access-token
 Entropy:     3.884400
 File:        uv.lock
 Line:        5129
 Fingerprint: uv.lock:square-access-***REDACTED_SECRET_ASSIGNMENT***
 �[90m10:03PM�[0m �[32mINF�[0m scan completed in 5.9s
 �[90m10:03PM�[0m �[31mWRN�[0m leaks found: 1
 ##[error]Process completed with exit code 1.

GitHub Actions: Agent completion enforcement / 0_Agent completion enforcement.txt: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const fs = require('fs');
const pull = await github.rest.pulls.get({
  owner: context.repo.owner,
  repo: context.repo.repo,
  pull_number: Number(process.env.PR)
});
let verdict = {
  conclusion: 'failure',
  reason: 'verifier_did_not_publish',
  details: {}
};
try {
  verdict = JSON.parse(fs.readFileSync(
    'enforcement-verdict.json', 'utf8'
  ));
} catch (error) {
  core.warning(error.message);
}
const conclusion = verdict.conclusion === 'success'
  ? 'success'
  : 'failure';
const summary = JSON.stringify(verdict);
await github.rest.checks.create({
  owner: context.repo.owner,
  repo: context.repo.repo,
  name: 'Agent completion enforcement',
  head_sha: pull.data.head.sha,
  status: 'completed',
  conclusion,
  output: {
    title: conclusion === 'success'
      ? 'Trusted evidence verified'
      : 'Trusted evidence blocked',
    summary: summary.slice(0, 60000)
  }
});
if (conclusion !== 'success') {
  core.setFailed(verdict.reason || 'trusted evidence blocked');
}
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   result-encoding: json
   retries: 0
   retry-exempt-status-codes: 400,401,403,404,422
 env:
   PR: 1188
 ##[endgroup]
 POST /repos/groupthinking/EventRelay/check-runs - 403 with id CC83:D5E37:163EF5E:4CA6CB2:6A6E6D9A in 239ms
 RequestError [HttpError]: API rate limit exceeded for installation. If you reach out to GitHub Support for help, please include the request ID CC83:D5E37:163EF5E:4CA6CB2:6A6E6D9A and timestamp  UTC. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) - https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#rate-limiting
     at fetchWrapper (/home/runner/work/_actions/actions/github-script/3a2844b7e9c422d3c10d287c895573f7108da1b3/...

GitHub Actions: Agent completion enforcement / Agent completion enforcement: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const fs = require('fs');
const pull = await github.rest.pulls.get({
  owner: context.repo.owner,
  repo: context.repo.repo,
  pull_number: Number(process.env.PR)
});
let verdict = {
  conclusion: 'failure',
  reason: 'verifier_did_not_publish',
  details: {}
};
try {
  verdict = JSON.parse(fs.readFileSync(
    'enforcement-verdict.json', 'utf8'
  ));
} catch (error) {
  core.warning(error.message);
}
const conclusion = verdict.conclusion === 'success'
  ? 'success'
  : 'failure';
const summary = JSON.stringify(verdict);
await github.rest.checks.create({
  owner: context.repo.owner,
  repo: context.repo.repo,
  name: 'Agent completion enforcement',
  head_sha: pull.data.head.sha,
  status: 'completed',
  conclusion,
  output: {
    title: conclusion === 'success'
      ? 'Trusted evidence verified'
      : 'Trusted evidence blocked',
    summary: summary.slice(0, 60000)
  }
});
if (conclusion !== 'success') {
  core.setFailed(verdict.reason || 'trusted evidence blocked');
}
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   result-encoding: json
   retries: 0
   retry-exempt-status-codes: 400,401,403,404,422
 env:
   PR: 1188
 ##[endgroup]
 POST /repos/groupthinking/EventRelay/check-runs - 403 with id CC83:D5E37:163EF5E:4CA6CB2:6A6E6D9A in 239ms
 RequestError [HttpError]: API rate limit exceeded for installation. If you reach out to GitHub Support for help, please include the request ID CC83:D5E37:163EF5E:4CA6CB2:6A6E6D9A and timestamp  UTC. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) - https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#rate-limiting
     at fetchWrapper (/home/runner/work/_actions/actions/github-script/3a2844b7e9c422d3c10d287c895573f7108da1b3/...

GitHub Actions: 🔍 Dependency Review / 0_dependency-review.txt: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/dependency-review-action@v5
 with:
   fail-on-severity: moderate
   allow-ghsas: GHSA-w5hq-g745-h8pq
   allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Python-2.0, BlueOak-1.0.0, MPL-2.0, CC0-1.0, 0BSD
   allow-dependencies-licenses: pkg:npm/caniuse-lite, pkg:npm/@sentry/nextjs, pkg:npm/@sentry/cli, pkg:npm/@sentry/cli-darwin, pkg:npm/@sentry/cli-linux-arm, pkg:npm/@sentry/cli-linux-arm64, pkg:npm/@sentry/cli-linux-i686, pkg:npm/@sentry/cli-linux-x64, pkg:npm/@sentry/cli-win32-arm64, pkg:npm/@sentry/cli-win32-i686, pkg:npm/@sentry/cli-win32-x64, pkg:npm/@sentry/bundler-plugin-core, pkg:npm/@sentry/babel-plugin-component-annotate
   comment-summary-in-pr: always
   repo-***REDACTED_SECRET_ASSIGNMENT***
 ##[endgroup]
 ##[error]Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled, see https://github.com/groupthinking/EventRelay/settings/security_analysis

GitHub Actions: 🔍 Dependency Review / dependency-review: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/dependency-review-action@v5
 with:
   fail-on-severity: moderate
   allow-ghsas: GHSA-w5hq-g745-h8pq
   allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Python-2.0, BlueOak-1.0.0, MPL-2.0, CC0-1.0, 0BSD
   allow-dependencies-licenses: pkg:npm/caniuse-lite, pkg:npm/@sentry/nextjs, pkg:npm/@sentry/cli, pkg:npm/@sentry/cli-darwin, pkg:npm/@sentry/cli-linux-arm, pkg:npm/@sentry/cli-linux-arm64, pkg:npm/@sentry/cli-linux-i686, pkg:npm/@sentry/cli-linux-x64, pkg:npm/@sentry/cli-win32-arm64, pkg:npm/@sentry/cli-win32-i686, pkg:npm/@sentry/cli-win32-x64, pkg:npm/@sentry/bundler-plugin-core, pkg:npm/@sentry/babel-plugin-component-annotate
   comment-summary-in-pr: always
   repo-***REDACTED_SECRET_ASSIGNMENT***
 ##[endgroup]
 ##[error]Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled, see https://github.com/groupthinking/EventRelay/settings/security_analysis

GitHub Actions: PR Checks / agent-completion_truth-gate: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const owner = context.repo.owner;
const repo = context.repo.repo;
const runUrl = context.serverUrl + '/' + owner + '/' + repo +
  '/actions/runs/' + context.runId;
let prNumber = Number(process.env.INPUT_PR_NUMBER || 0);
if (!prNumber && context.payload.pull_request) {
  prNumber = context.payload.pull_request.number;
}
if (!prNumber) {
  core.setOutput('pr_number', '');
  core.setOutput('head_sha', '');
  core.setOutput('base_sha', '');
  return;
}
if (context.payload.pull_request &&
    context.payload.pull_request.head) {
  core.setOutput('pr_number', String(prNumber));
  core.setOutput(
    'head_sha',
    context.payload.pull_request.head.sha
  );
}
const pr = (await github.rest.pulls.get({
  owner,
  repo,
  pull_number: prNumber
})).data;
core.setOutput('pr_number', String(prNumber));
core.setOutput('head_sha', pr.head.sha);
core.setOutput('base_sha', pr.base.sha);
const gateContext =
  'agent-completion/truth-gate/pr-' + prNumber;
const statuses = await github.paginate(
  github.rest.repos.listCommitStatusesForRef,
  {owner, repo, ref: pr.head.sha, per_page: 100}
);
const gateStatuses = statuses.filter(status =>
  status.context === gateContext
);
if (gateStatuses.length >= 998) {
  core.setOutput('pending_status_id', '');
  core.setFailed(
    'status_capacity_exhausted: push a new head or complete `#874`'
  );
  return;
}
const pendingStatus = await github.rest.repos.createCommitStatus({
  owner,
  repo,
  sha: pr.head.sha,
  state: 'pending',
  context: gateContext,
  description: 'collecting repository evidence',
  target_url: runUrl
});
core.setOutput(
  'pending_status_id',
  String(pendingStatus.data.id)
);
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   result-encoding: json
   retries: 0
   retry-exempt-status-codes: 400,401,403,404,422
 env:
   INPUT_PR_NUMBER:
 ##[endgroup]
 GET /repos/groupthinking/Even...

GitHub Actions: PR Checks / agent-completion_truth-gate: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
 with:
   name: agent-completion-verdict-1188
   path: gate-input.json
gate-verdict.json
   if-no-files-found: error
   compression-level: 6
   overwrite: false
   include-hidden-files: false
   archive: true
 ##[endgroup]
 Multiple search paths detected. Calculating the least common ancestor of all paths
 The least common ancestor is /home/runner/work/EventRelay/EventRelay. This will be the root directory of the artifact
 ##[error]No files were found with the provided path: gate-input.json

GitHub Actions: PR Checks / agent-completion_truth-gate: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const fs = require('fs');
const owner = context.repo.owner;
const repo = context.repo.repo;
const marker = '<!-- agent-completion-truth-gate:v1 -->';
const runUrlPrefix = context.serverUrl + '/' + owner + '/' +
  repo + '/actions/runs/';
const runUrl = runUrlPrefix + context.runId;
const gateContext = 'agent-completion/truth-gate/pr-' +
  process.env.PR_NUMBER;
function gateStatusDisposition(
  status,
  expectedPendingId,
  currentRunUrl,
  targetPrefix
) {
  if (!/^\d+$/.test(String(expectedPendingId || '')) ||
      !status || !/^\d+$/.test(String(status.id || ''))) {
    return 'fail_closed';
  }
  const target = String(
    (status && status.target_url) || ''
  );
  const expectedId = BigInt(String(expectedPendingId));
  const statusId = BigInt(String(status.id));
  function validRunTarget(targetUrl) {
    const value = String(targetUrl || '');
    if (!value.startsWith(targetPrefix)) {
      return false;
    }
    const suffix = value.slice(targetPrefix.length);
    return /^\d+$/.test(suffix);
  }
  function statusOwnerId(candidate) {
    if (candidate.state === 'pending') {
      return BigInt(String(candidate.id));
    }
    const owner = String(candidate.description || '').match(
      /^gate-owner:(\d+)(?:\s|$)/
    );
    return owner ? BigInt(owner[1]) : null;
  }
  if (!validRunTarget(currentRunUrl) ||
      !validRunTarget(target)) {
    return 'fail_closed';
  }
  const ownerId = statusOwnerId(status);
  if (ownerId === null) {
    return 'fail_closed';
  }
  if (ownerId === expectedId && target === currentRunUrl) {
    if (statusId === expectedId &&
        status.state === 'pending') {
      return 'current_pending';
    }
    if (['failure', 'error'].includes(status.state)) {
      return 'already_failed';
    }
    if (status.state === 'success') {
      return 'already_succeeded';
    }
    return 'fail_closed';
  }
  if (target === currentRunUrl) {...

GitHub Actions: PR Checks / agent-completion_truth-gate: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run exit 1
 �[36;1mexit 1�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
 ##[error]Process completed with exit code 1.

GitHub Actions: PR Checks / validate: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const pr = context.payload.pull_request;
const findings = [];
if (pr.title.length < 10) {
  findings.push('❌ PR title too short (minimum 10 characters)');
}
if (!/^(?:⚡\s*)?(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?:/i.test(pr.title)) {
  findings.push('⚠️ PR title should follow conventional commits format');
}
if (!pr.body || pr.body.length < 20) {
  findings.push('❌ PR description is required (minimum 20 characters)');
}
const totalChanges = (pr.additions || 0) + (pr.deletions || 0);
if (totalChanges > 500) {
  findings.push('⚠️ Large PR detected (' + totalChanges + ' lines changed)');
}
const marker = '<!-- pr-validation:v1 -->';
const comments = await github.paginate(
  github.rest.issues.listComments,
  {owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, per_page: 100}
);
const existing = comments.find(comment =>
  comment.user &&
  comment.user.login === 'github-actions[bot]' &&
  comment.body && comment.body.includes(marker)
);
if (findings.length === 0) {
  if (existing) {
    await github.rest.issues.updateComment({
      owner: context.repo.owner,
      repo: context.repo.repo,
      comment_id: existing.id,
      body: marker + '\n## 🔍 PR Validation\n\n' +
        '✅ Current validation passed.'
    });
  }
  return;
}
const body = marker + '\n## 🔍 PR Validation\n\n' + findings.join('\n');
if (existing) {
  await github.rest.issues.updateComment({
    owner: context.repo.owner,
    repo: context.repo.repo,
    comment_id: existing.id,
    body
  });
} else {
  await github.rest.issues.createComment({
    owner: context.repo.owner,
    repo: context.repo.repo,
    issue_number: pr.number,
    body
  });
}
if (findings.some(finding => finding.startsWith('❌'))) {
  core.setFailed('PR validation failed');
}
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   resu...

GitHub Actions: PR Checks / 0_agent-completion_truth-gate.txt: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const owner = context.repo.owner;
const repo = context.repo.repo;
const runUrl = context.serverUrl + '/' + owner + '/' + repo +
  '/actions/runs/' + context.runId;
let prNumber = Number(process.env.INPUT_PR_NUMBER || 0);
if (!prNumber && context.payload.pull_request) {
  prNumber = context.payload.pull_request.number;
}
if (!prNumber) {
  core.setOutput('pr_number', '');
  core.setOutput('head_sha', '');
  core.setOutput('base_sha', '');
  return;
}
if (context.payload.pull_request &&
    context.payload.pull_request.head) {
  core.setOutput('pr_number', String(prNumber));
  core.setOutput(
    'head_sha',
    context.payload.pull_request.head.sha
  );
}
const pr = (await github.rest.pulls.get({
  owner,
  repo,
  pull_number: prNumber
})).data;
core.setOutput('pr_number', String(prNumber));
core.setOutput('head_sha', pr.head.sha);
core.setOutput('base_sha', pr.base.sha);
const gateContext =
  'agent-completion/truth-gate/pr-' + prNumber;
const statuses = await github.paginate(
  github.rest.repos.listCommitStatusesForRef,
  {owner, repo, ref: pr.head.sha, per_page: 100}
);
const gateStatuses = statuses.filter(status =>
  status.context === gateContext
);
if (gateStatuses.length >= 998) {
  core.setOutput('pending_status_id', '');
  core.setFailed(
    'status_capacity_exhausted: push a new head or complete `#874`'
  );
  return;
}
const pendingStatus = await github.rest.repos.createCommitStatus({
  owner,
  repo,
  sha: pr.head.sha,
  state: 'pending',
  context: gateContext,
  description: 'collecting repository evidence',
  target_url: runUrl
});
core.setOutput(
  'pending_status_id',
  String(pendingStatus.data.id)
);
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   result-encoding: json
   retries: 0
   retry-exempt-status-codes: 400,401,403,404,422
 env:
   INPUT_PR_NUMBER:
 ##[endgroup]
 GET /repos/groupthinking/Even...

GitHub Actions: PR Checks / agent-completion_truth-gate: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const owner = context.repo.owner;
const repo = context.repo.repo;
const runUrlPrefix = context.serverUrl + '/' + owner + '/' +
  repo + '/actions/runs/';
const runUrl = runUrlPrefix + context.runId;
const gateContext = 'agent-completion/truth-gate/pr-' +
  process.env.PR_NUMBER;
function gateStatusDisposition(
  status,
  expectedPendingId,
  currentRunUrl,
  targetPrefix
) {
  if (!/^\d+$/.test(String(expectedPendingId || '')) ||
      !status || !/^\d+$/.test(String(status.id || ''))) {
    return 'fail_closed';
  }
  const target = String(
    (status && status.target_url) || ''
  );
  const expectedId = BigInt(String(expectedPendingId));
  const statusId = BigInt(String(status.id));
  function validRunTarget(targetUrl) {
    const value = String(targetUrl || '');
    if (!value.startsWith(targetPrefix)) {
      return false;
    }
    const suffix = value.slice(targetPrefix.length);
    return /^\d+$/.test(suffix);
  }
  function statusOwnerId(candidate) {
    if (candidate.state === 'pending') {
      return BigInt(String(candidate.id));
    }
    const owner = String(candidate.description || '').match(
      /^gate-owner:(\d+)(?:\s|$)/
    );
    return owner ? BigInt(owner[1]) : null;
  }
  if (!validRunTarget(currentRunUrl) ||
      !validRunTarget(target)) {
    return 'fail_closed';
  }
  const ownerId = statusOwnerId(status);
  if (ownerId === null) {
    return 'fail_closed';
  }
  if (ownerId === expectedId && target === currentRunUrl) {
    if (statusId === expectedId &&
        status.state === 'pending') {
      return 'current_pending';
    }
    if (['failure', 'error'].includes(status.state)) {
      return 'already_failed';
    }
    if (status.state === 'success') {
      return 'already_succeeded';
    }
    return 'fail_closed';
  }
  if (target === currentRunUrl) {
    return 'fail_closed';
  }
  if (ownerId > expectedId) {
    return 'successor';...

GitHub Actions: PR Checks / 2_validate.txt: perf(cloud-ai): analyse each batch concurrently in process_batch_videos

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const pr = context.payload.pull_request;
const findings = [];
if (pr.title.length < 10) {
  findings.push('❌ PR title too short (minimum 10 characters)');
}
if (!/^(?:⚡\s*)?(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?:/i.test(pr.title)) {
  findings.push('⚠️ PR title should follow conventional commits format');
}
if (!pr.body || pr.body.length < 20) {
  findings.push('❌ PR description is required (minimum 20 characters)');
}
const totalChanges = (pr.additions || 0) + (pr.deletions || 0);
if (totalChanges > 500) {
  findings.push('⚠️ Large PR detected (' + totalChanges + ' lines changed)');
}
const marker = '<!-- pr-validation:v1 -->';
const comments = await github.paginate(
  github.rest.issues.listComments,
  {owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, per_page: 100}
);
const existing = comments.find(comment =>
  comment.user &&
  comment.user.login === 'github-actions[bot]' &&
  comment.body && comment.body.includes(marker)
);
if (findings.length === 0) {
  if (existing) {
    await github.rest.issues.updateComment({
      owner: context.repo.owner,
      repo: context.repo.repo,
      comment_id: existing.id,
      body: marker + '\n## 🔍 PR Validation\n\n' +
        '✅ Current validation passed.'
    });
  }
  return;
}
const body = marker + '\n## 🔍 PR Validation\n\n' + findings.join('\n');
if (existing) {
  await github.rest.issues.updateComment({
    owner: context.repo.owner,
    repo: context.repo.repo,
    comment_id: existing.id,
    body
  });
} else {
  await github.rest.issues.createComment({
    owner: context.repo.owner,
    repo: context.repo.repo,
    issue_number: pr.number,
    body
  });
}
if (findings.some(finding => finding.startsWith('❌'))) {
  core.setFailed('PR validation failed');
}
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   resu...

Commit Status: Vercel: Vercel

Conclusion: failure

Canceled from the Vercel Dashboard
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{py,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,js,jsx,ts,tsx}: Use Python 3.9+ and Node 18+ for development
Never hardcode API keys, database URLs, or secrets in code
Make minimal, surgical changes and avoid deleting working code unless fixing security issues

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Always use type hints for Python functions
Use Black formatter with 88 character line length for Python code
Follow PEP 8 conventions for Python code
Use descriptive variable names and add docstrings to all public functions in Python
Group imports in Python: standard library, third-party, local
Use SQLAlchemy ORM and never write raw SQL queries
Use environment variables via os.getenv() or pydantic-settings to access configuration
Wrap database operations in try-except blocks and use context managers or FastAPI dependencies for connection cleanup
Implement comprehensive error handling with proper logging in all functions
Version APIs using /api/v1/ prefix for stability
Use JSON-RPC 2.0 protocol for all MCP communication
Follow the single-flow workflow: YouTube link → context extraction → agent dispatch → outputs
Use context managers for resource management in Python code
Implement comprehensive input validation and sanitize outputs for security
Use parameterized queries and SQLAlchemy ORM to prevent SQL injection
Define API request/response models using Pydantic for FastAPI endpoints
Store the single unified workflow as the only workflow; never introduce alternate flows or manual triggers
Use SQLAlchemy with connection pooling for database connections and manage sessions with context managers
Provide sensible defaults for non-sensitive configuration in Python settings
Maintain backward compatibility and do not break existing API endpoints
Include comprehensive logging for debugging in MCP implementations

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py

⚙️ CodeRabbit configuration file

Python backend code. Check for type hints, proper exception handling, async context manager usage, and potential blocking calls in async functions. Flag any bare except clauses or missing timeout parameters on network calls. CRITICAL: Flag any file that contains placeholder/stub implementations — especially in unified_ai_sdk. Any class or function that says "TODO: Replace with production implementation" or returns mock/fake data must be flagged as a blocking issue. Flag any code generation output that reaches users without AST validation or syntax checking.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*.{py,js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Maintain >80% code coverage for new features

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,ts,tsx}: Keep frontend and backend data models synchronized using matching Pydantic (backend) and TypeScript (frontend) interfaces
Use type-safe interfaces for backend-frontend data exchange

**/*.{py,ts,tsx}: Production code must use real behavior only: no mock delays, fake data, or simulated responses.
Maintain strict type safety: mypy strict mode for Python and TypeScript strict mode for the frontend.
Name events using the <domain>.<entity>.<action> format.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*

📄 CodeRabbit inference engine (Custom checks)

**/*: Strictly verify that GitHub Copilot has explicitly reviewed and approved the pull request; human approvals alone must not satisfy this check.
Before allowing a merge, require the copilot-rabbit label and AI-generated unit tests committed alongside the code changes; fail the check if either is missing.

For Vercel-specific work, include https://vercel.com/docs/llms-full.txt in the AI assistant context set.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*.{py,pyw}

📄 CodeRabbit inference engine (AGENTS.md)

Write Python code to remain compatible with Linux and Windows where possible, including correct handling of asyncio event loops.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Format Python code with Black using an 88-character line length.
Sort Python imports with isort using the Black profile.
Run Ruff with E, W, F, I, B, C4, and UP rules; E501 is ignored.
Use mypy strict mode; untyped function definitions are disallowed.
Target Python 3.9 or newer.
Validate backend inputs with Pydantic and sanitize subprocess arguments.
Use Anthropic SDK features thinking={"type": "adaptive"} and output_config={"effort": "..."} with anthropic>=0.105.0; do not add TypeError fallbacks for these parameters.

Use the service container dependency-injection pattern in backend/containers/.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*.{py,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit secrets; store keys and credentials in gitignored .env files.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi}: Use Black formatting with an 88-character line length for Python code.
Use Ruff with rules E, W, F, I, B, C4, and UP for Python linting.
Use strict mypy type checking; do not define untyped functions.
Target Python 3.9 or newer.
Validate Python inputs with Pydantic.
Sanitize subprocess arguments before execution.
Do not add mock delays or fake data to production Python code; production runs in REAL_MODE_ONLY.
Keep secrets out of Python source code; load keys and credentials from environment variables instead.
Use absolute imports that resolve with PYTHONPATH=src in the Python backend.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
**/*.{py,pyi,ts,tsx}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi,ts,tsx}: Preserve the single workflow: YouTube link → transcript → events → agents → outputs; do not introduce alternative flows or manual triggers that bypass it.
Use event names following <domain>.<entity>.<action>, such as youtube.video.captured.
Make surgical, precise changes and do not delete working code without justification.

Files:

  • src/youtube_extension/backend/cloud_ai_routes.py
🔍 Remote MCP GitHub Copilot

Additional review context

  • The linked issue is perf(cloud-ai): process_batch_videos analyses each batch sequentially, making batch_size inert #1187; the PR is #1188. The issue requires per-batch concurrency, batch_size as the in-flight bound, failure isolation, preserved pauses, module-level asyncio, and mutation-resistant tests.
  • PR #1188 changes only cloud_ai_routes.py and adds tests/unit/test_cloud_ai_routes_batch.py. The implementation uses one CloudAIIntegrator per batch-processing invocation and gathers each sliced batch concurrently, preserving result order with zip(..., strict=True).
  • CloudAIIntegrator.analyze_video() routes each call through the configured provider and its shared provider instance; the batch therefore exercises the same integrator/provider objects concurrently.
  • pyproject.toml requires Python >=3.10, and the production Dockerfile uses Python 3.11, so the new zip(strict=True) usage is compatible with the declared runtime.
  • The prior cancellation review thread remains marked unresolved, although the current diff includes the proposed BaseException cancellation re-raise and two regression tests.
  • CI was mixed at retrieval time: Python lint, build, security bandit, npm audit, and safety passed; test and coverage jobs were still running. Dependency review, gitleaks, validation/truth-gate, and agent-completion checks reported failures.
🔇 Additional comments (2)
src/youtube_extension/backend/cloud_ai_routes.py (2)

7-7: LGTM!


381-425: LGTM!


📝 Walkthrough

Summary by CodeRabbit

  • Performance Improvements

    • Batch video analysis now processes multiple videos concurrently, reducing overall processing time.
    • Individual video failures are skipped while allowing the remaining batch to complete.
  • Reliability

    • Cancellation behavior is preserved during batch processing.

Walkthrough

process_batch_videos now analyzes videos in each batch concurrently with asyncio.gather. It preserves per-video exception handling, cancellation propagation, result formatting, and the pause between batches.

Changes

Cloud AI batch processing

Layer / File(s) Summary
Concurrent batch execution
src/youtube_extension/backend/cloud_ai_routes.py
The module imports asyncio. Video analyses within each batch run concurrently. Ordinary per-video exceptions are logged and skipped, cancellation-like exceptions are re-raised, successful results are formatted, and the inter-batch pause remains.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested labels: copilot-rabbit

Suggested reviewers: claude

Poem

Videos gather in flight,
Failures fade from sight,
Cancellations break through,
Results return anew,
Then batches pause just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 3

❌ Failed checks (3 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The implementation matches issue #1187, but the required regression tests cannot be verified because the test file is excluded by !tests/**. Include reviewable evidence for tests/unit/test_cloud_ai_routes_batch.py or remove the !tests/** exclusion so the regression tests can be verified.
Enforce Copilot Verification ❓ Inconclusive Evidence collection is still in progress. Inspect the pull request review records and verify an explicit approval from GitHub Copilot.
Require Ai Unit Tests ❓ Inconclusive The repository confirms committed batch tests, but it contains no pull-request label state; external PR metadata is required to verify copilot-rabbit. Provide the pull request URL or accessible GitHub metadata showing the current PR labels.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: concurrent per-batch analysis in cloud AI processing.
Description check ✅ Passed The description covers the change, scope, risks, verification, production reachability, and handoff with sufficient detail.
Out of Scope Changes check ✅ Passed The reviewed changes support issue #1187 and contain no unrelated production code changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/batch-video-analyze
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch perf/batch-video-analyze

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were analyzed; skipped groupthinking/uvai-skills.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 757e847.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Please review with particular attention to these five points — I would rather have them challenged now than after merge:

  1. Is batch_size a sufficient concurrency bound on its own? I deliberately did not add a semaphore, arguing that gathering within a batch while keeping batches sequential already caps in-flight provider calls at batch_size. Is there a path where peak concurrency exceeds batch_size — e.g. if analyze_video internally fans out further, or if use_fallback=True issues a second provider call while the first is still counted?

  2. isinstance(result, Exception) vs the original except Exception:. I claim exact parity because CancelledError is a BaseException. But gather(return_exceptions=True) also captures CancelledError of child tasks in some versions rather than propagating. Does this change behaviour if the background task is cancelled mid-batch?

  3. Resource contention. All videos in a batch now hit one shared CloudAIIntegrator (and its aiohttp session) simultaneously. Is there per-instance state in CloudAIIntegrator.analyze_video that is not safe under concurrent entry?

  4. Rate limiting. Bursting batch_size requests at once is more likely to trip a provider rate limit than the previous drip-feed, even though total call count is unchanged. Should RateLimitError be retried within the batch rather than logged and dropped, or is that correctly out of scope here?

  5. Test honesty. 3 of my 5 tests pass against both the old and new implementation. I have documented them as regression guards rather than claiming them as proof. Do the two that do fail (peak-concurrency assertions) adequately pin the behaviour, or is there a cheaper implementation that would pass them without actually being concurrent?

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

Machine-readable verdict
{
  "details": {},
  "reasons": [],
  "verdict": "not_applicable"
}

Workflow evidence

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Changed files =='
git diff --name-only origin/main...HEAD

echo '== Route and integrator outlines =='
ast-grep outline src/youtube_extension/backend/cloud_ai_routes.py --items all --match 'process_batch_videos|analyze_video' --view expanded || true
fd -i 'cloud_ai*' src tests | sort

echo '== Batch implementation and endpoint context =='
rg -n -C 12 'async def process_batch_videos|process_batch_videos\s*\(|analyze/batch|background_tasks\.add_task' \
  src/youtube_extension/backend/cloud_ai_routes.py

echo '== Integrator definitions, fallback, provider calls, mutable instance state, and rate-limit handling =='
rg -n -C 8 --glob '*.py' \
  'class CloudAIIntegrator|async def analyze_video|def analyze_video|use_fallback|fallback|RateLimitError|rate.limit|aiohttp\.ClientSession|self\.[A-Za-z_]+\s*=' \
  src/youtube_extension

echo '== New batch test outline and relevant test body =='
ast-grep outline tests/unit/test_cloud_ai_routes_batch.py --items all --view expanded || true
cat -n tests/unit/test_cloud_ai_routes_batch.py

echo '== Exact PR diff =='
git diff --unified=80 origin/main...HEAD -- \
  src/youtube_extension/backend/cloud_ai_routes.py \
  tests/unit/test_cloud_ai_routes_batch.py

Length of output: 201880


🏁 Script executed:

#!/bin/bash
set -u

echo '== Changed paths =='
git diff --name-only origin/main...HEAD 2>&1 | sed -n '1,100p'

echo '== Candidate route, integrator, and batch-test paths =='
fd -t f -i 'cloud_ai_routes.py' . | sort
fd -t f -i '*cloud*ai*integrator*.py' src tests | sort
fd -t f -i '*cloud*ai*route*batch*.py' tests . | sort

echo '== PR diff summary =='
git diff --stat origin/main...HEAD 2>&1 | sed -n '1,120p'

echo '== Only relevant diff lines =='
git diff --unified=35 origin/main...HEAD -- \
  ':(glob)**/cloud_ai_routes.py' \
  ':(glob)**/test_cloud_ai_routes_batch.py' 2>&1 | sed -n '1,800p'

route="$(fd -t f -i 'cloud_ai_routes.py' src | head -n 1)"
if [ -n "$route" ]; then
  echo "== $route: batch code =="
  rg -n -C 20 'async def process_batch_videos|process_batch_videos\s*\(|asyncio\.gather|use_fallback' "$route" | sed -n '1,500p'
fi

echo '== CloudAIIntegrator candidates and definitions =='
rg -n -l --glob '*.py' 'class CloudAIIntegrator\b' src tests 2>/dev/null | while IFS= read -r f; do
  echo "== $f =="
  rg -n -C 30 'class CloudAIIntegrator\b|async def analyze_video\b|def analyze_video\b|use_fallback|asyncio\.gather|self\.[A-Za-z_]+\s*=' "$f" | sed -n '1,1000p'
done

test="$(fd -t f -i 'test_cloud_ai_routes_batch.py' . | head -n 1)"
if [ -n "$test" ]; then
  echo "== $test =="
  cat -n "$test" | sed -n '1,700p'
fi

Length of output: 27764


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Python/runtime metadata =='
for f in pyproject.toml setup.cfg setup.py requirements.txt requirements-dev.txt; do
  if [ -f "$f" ]; then
    echo "== $f =="
    rg -n -C 3 'python|aiohttp|google|boto|azure' "$f" || true
  fi
done

echo '== Integrator initialization methods and full analyze_video body =='
sed -n '42,135p' src/youtube_extension/integrations/cloud_ai/integrator.py
rg -n -C 25 'async def _initialize_(google_cloud|aws_rekognition|azure_vision)' \
  src/youtube_extension/integrations/cloud_ai/integrator.py

echo '== Cloud AI provider source files =='
fd -t f -e py . src/youtube_extension/integrations/cloud_ai/providers | sort

for f in $(fd -t f -e py . src/youtube_extension/integrations/cloud_ai/providers | sort); do
  echo "== $f: analyze_video and mutable state =="
  rg -n -C 20 \
    'async def analyze_video\b|async def initialize\b|async def cleanup\b|self\.[A-Za-z_]+\s*=|asyncio\.gather|create_task|RateLimitError|retry|sleep\(' \
    "$f" | sed -n '1,700p'
done

echo '== Existing tests for integrator cancellation, fallback, and rate limits =='
rg -n -C 8 --glob '*.py' \
  'CancelledError|return_exceptions|RateLimitError|retry_after|use_fallback|analyze_video' \
  src/youtube_extension/integrations/cloud_ai tests/unit 2>/dev/null | sed -n '1,1000p'

echo '== Batch test path and cancellation/rate-limit coverage =='
git show HEAD:tests/unit/test_cloud_ai_routes_batch.py | nl -ba | sed -n '1,220p'

Length of output: 14804


🏁 Script executed:

#!/bin/bash
set -u

for f in \
  src/youtube_extension/integrations/cloud_ai/providers/google_cloud.py \
  src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py \
  src/youtube_extension/integrations/cloud_ai/providers/azure_vision.py; do
  echo "== $f: outline =="
  ast-grep outline "$f" --items all --view expanded 2>&1 | sed -n '1,260p'

  echo "== $f: complete bounded source =="
  nl -ba "$f" | sed -n '1,520p'
done

echo '== All direct provider calls and per-call task fan-out in provider modules =='
rg -n -C 8 --glob '*.py' \
  '\.(annotate_video|annotate_image|start_label_detection|start_face_detection|start_person_tracking|start_segment_detection|analyze_video|analyze_image)\s*\(|asyncio\.(gather|create_task)|run_in_executor|to_thread' \
  src/youtube_extension/integrations/cloud_ai/providers 2>&1 | sed -n '1,900p'

echo '== Rate-limit handling and retry loops in the Cloud AI path =='
rg -n -C 10 --glob '*.py' \
  'RateLimitError|retry_after|for .*range.*retry|backoff|await asyncio\.sleep' \
  src/youtube_extension/integrations/cloud_ai 2>&1 | sed -n '1,900p'

echo '== Test names and async test configuration =='
git ls-tree -r --name-only HEAD | rg '(^|/)test_cloud_ai_(routes_batch|integrator|exceptions|config)\.py$' || true
git show HEAD:tests/unit/test_cloud_ai_routes_batch.py | nl -ba | sed -n '1,180p'

Length of output: 49397


🏁 Script executed:

#!/bin/bash
set -u

echo '== Formatter contract and batch endpoint =='
rg -n -C 35 \
  '^def format_analysis_result\b|^async def format_analysis_result\b|`@router`\.post\("/analyze/batch"|background_tasks\.add_task|process_batch_videos' \
  src/youtube_extension/backend/cloud_ai_routes.py 2>&1 | sed -n '1,700p'

echo '== Attributes read from a VideoAnalysisResult by the formatter =='
sed -n '1,260p' src/youtube_extension/backend/cloud_ai_routes.py

echo '== Repository references to child-task cancellation handling =='
rg -n -C 8 --glob '*.py' \
  'gather\(.+return_exceptions=True|isinstance\(.*BaseException|except asyncio\.CancelledError|raise.*CancelledError' \
  src tests 2>&1 | sed -n '1,800p'

Length of output: 83487


@groupthinking I found one correctness issue.

Blocking — cancellation is not fully preserved.
At Line 404, isinstance(result, Exception) does not handle a child asyncio.CancelledError. With return_exceptions=True, asyncio.gather can return that cancellation as a result value when one child is cancelled independently. The next line passes it to format_analysis_result, which expects VideoAnalysisResult fields such as video_id. The formatter raises AttributeError. The outer handler logs that error and ends the background task.

The old sequential loop would propagate CancelledError, because its except Exception clause did not catch it. Preserve that behavior by re-raising any BaseException result before the Exception failure-isolation path. Add a test where one fake analyze_video raises asyncio.CancelledError and assert that process_batch_videos raises it.

My assessment of the five points follows.

  1. batch_size bound
    batch_size is sufficient as a per-process_batch_videos invocation bound. The function awaits the complete gather before it starts the next batch. One CloudAIIntegrator.analyze_video call does not fan out concurrently. Its fallback provider calls are serial. Therefore, fallback does not cause one video to consume two concurrent provider calls.

    It is not a global provider bound. Each POST /analyze/batch call creates a separate background task and a separate integrator. Two simultaneous batch requests with batch_size=5 can create up to ten in-flight per-video analyses. If the intended contract is global quota protection, use an application-wide, provider-keyed limiter. That work is outside this latency-focused PR.

    The low-level provider request count also differs by provider. AWS can start multiple Rekognition jobs for one video when several analysis types are requested. Thus, batch_size bounds concurrent video analyses, not necessarily all provider operations in every implementation.

  2. Exception parity and cancellation
    For cancellation of the parent background task, gather is cancelled and cancellation propagates. return_exceptions=True does not suppress parent cancellation. This is correct.

    For cancellation of an individual child, the current result loop is not equivalent to the former except Exception behavior. This is the blocking issue above. Handle BaseException results first, then retain Exception for per-video failures.

  3. Resource contention
    I found no per-video mutable state on CloudAIIntegrator that makes concurrent entry unsafe. Provider setup occurs in __aenter__ before the batch begins. Each successful result object receives its own provider value.

    The provider implementations have a separate performance limitation. AWS Rekognition uses synchronous boto3 calls inside async functions. Azure uses synchronous Computer Vision calls for several frame-analysis paths. Those calls can block the event loop. gather still schedules the coroutines concurrently, but those provider paths may not achieve the expected wall-clock improvement. This is pre-existing behavior and does not make the new fan-out unsafe. The new tests only prove route-level coroutine concurrency, not concurrent provider I/O.

  4. Rate limiting
    RateLimitError has no retry policy in this path. CloudAIIntegrator.analyze_video catches it as Exception and proceeds to the next fallback provider. If all providers fail, the batch logs and drops that video.

    I agree that provider-specific retries are out of scope for this PR. Retries need provider-specific backoff, retry_after support, and coordination with a global quota limiter. Adding a local retry here could increase the burst and duplicate the policy later. File a follow-up if retries are required.

  5. Test honesty
    The two peak-concurrency tests adequately reject the old sequential implementation. A replacement using TaskGroup, explicit tasks, or another concurrent primitive could also pass. That is acceptable because the required behavior is concurrent batch processing, not use of asyncio.gather.

    The tests do not pin:

    • cancellation propagation;
    • result ordering;
    • real provider I/O concurrency; or
    • global quota behavior across simultaneous batch tasks.

    Add the cancellation test as part of the blocking fix. An ordering test is low cost and would protect the documented gather ordering contract. Provider I/O and global quota tests belong to separate provider and rate-limit work.

🐇

You are interacting with an AI system.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves batch video-analysis latency by running each batch concurrently while preserving ordering and failure isolation.

Changes:

  • Uses asyncio.gather for bounded concurrent analysis.
  • Adds tests for concurrency, batching, failures, pauses, and empty input.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/youtube_extension/backend/cloud_ai_routes.py Adds concurrent batch processing.
tests/unit/test_cloud_ai_routes_batch.py Adds batch-processing regression tests.

Comment thread src/youtube_extension/backend/cloud_ai_routes.py

@groupthinking groupthinking left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review — verified the blocking finding and one accuracy caveat at head 757e8475.

I checked the two material claims against the source rather than echoing the bot.

1. Correctness — child CancelledError is mishandled (confirms CodeRabbit; contradicts this PR's own rationale).

Verified the mechanism, not just the conclusion: asyncio.gather(..., return_exceptions=True) appends a cancelled child's CancelledError as a result element — CPython's gather._done_callback builds the results list with a CancelledError for any fut.cancelled(), it does not re-raise it. At cloud_ai_routes.py:404, isinstance(result, Exception) is False for CancelledError (it derives from BaseException), so control falls through to format_analysis_result(result)AttributeError (no .video_id) → swallowed by the function-level except Exception at :418.

This is the exact opposite of the PR body's stated design ("cancellation still propagates rather than being silently swallowed as a failed video"). The old await ai.analyze_video() under except Exception: did let a CancelledError propagate; the new code does not. Real-world trigger probability is low here (nothing external holds the internal gather children to cancel them individually), but since the PR documents the opposite guarantee, code and doc need reconciling either way. Minimal fix:

for video_url, result in zip(batch, batch_results, strict=True):
    if isinstance(result, BaseException) and not isinstance(result, Exception):
        raise result  # CancelledError / KeyboardInterrupt / SystemExit — propagate, as before
    if isinstance(result, Exception):
        logger.error(f"Failed to analyze video {video_url}: {result}")
        continue
    results.append(format_analysis_result(result))

For the test (your point 5): a fake analyze_video raising asyncio.CancelledError must assert that process_batch_videos re-raises it — note that with format_analysis_result mocked to identity the current code would neither raise nor crash, so an assertion on "formatter error" would be a false pass; assert propagation.

2. Accuracy — the "~slowest single analysis" wall-clock claim holds for only 2 of the 3 providers.

The before/after table promises batch wall-clock drops to the slowest single analysis. Verified this is provider-dependent:

  • Google (providers/google_cloud.py): native async client (await self._video_client.annotate_video, await asyncio.wait_for(operation.result(), ...)) → genuinely concurrent under gather. ✓
  • Azure (providers/azure_vision.py:273): offloads blocking SDK calls via asyncio.to_thread → genuinely concurrent. ✓
  • AWS Rekognition (providers/aws_rekognition.py): synchronous boto3 calls made directly on the event loopstart_label_detection / get_*_detection at lines 281–335, no to_thread/run_in_executor. These block the loop, so gather cannot overlap the RPCs; only the await asyncio.sleep(poll_interval) at :345 interleaves. For an AWS-served batch the wall-clock stays ~sum of the synchronous RPC latencies, and the batch now holds the loop across those calls.

This doesn't make the change unsafe — failure isolation, ordering, and the inter-batch pause are all preserved — but the perf outcome is overstated for the AWS path, and the new tests (which mock analyze_video) prove route-level coroutine scheduling, not real provider-I/O concurrency. Suggest either scoping the claim to the async providers in the PR body, or wrapping the sync boto3 calls in to_thread as a separate change.

On your five questions: answers to 1, 4, 5 match my read — batch_size bounds concurrency per process_batch_videos invocation only, not globally across simultaneous /analyze/batch requests (each spins its own integrator); RateLimitError retry is correctly out of scope; and the two peak-concurrency assertions do reject the sequential implementation. Item 2 is the confirmed bug above. Item 3 — no unsafe per-video shared state on CloudAIIntegrator, but see the AWS blocking caveat.

Net: one correctness fix (item 1) to match the documented intent, and one honest scoping of the perf claim (item 2). Both small; neither is a redesign.


Generated by Claude Code

`asyncio.gather(..., return_exceptions=True)` captures a child's
CancelledError as a *value*. Because it derives from BaseException and
not Exception, the `isinstance(result, Exception)` filter let it fall
through to `format_analysis_result`, which raised AttributeError that
the outer `except Exception` then swallowed -- silently skipping every
remaining batch.

Re-raise BaseException-but-not-Exception results before the per-video
failure handling, restoring the propagation semantics of the original
sequential `await`/`except Exception` loop.

Reported by @Copilot in review of #1188. Adds 2 regression tests that
fail with "DID NOT RAISE CancelledError" without this change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@groupthinking

Copy link
Copy Markdown
Owner Author

@copilot — this finding is correct, and it was worse than a swallowed cancellation. Thank you.

You are right that gather(..., return_exceptions=True) captures a child's CancelledError as a
value, and that isinstance(result, Exception) is False for it — so it fell straight through
to format_analysis_result. That raised an AttributeError, which my own outer except Exception
then swallowed, silently abandoning every remaining batch. The original sequential
await / except Exception loop let cancellation propagate.

Fixed at 8725b51d:

# `return_exceptions=True` also captures BaseException-only failures
# (most notably CancelledError) as *values*, so an
# `isinstance(..., Exception)` test alone would let a cancelled child
# fall through to format_analysis_result. Re-raise those to preserve
# the previous `await`/`except Exception` semantics.
cancellations = [
    result
    for result in batch_results
    if isinstance(result, BaseException) and not isinstance(result, Exception)
]
if cancellations:
    raise cancellations[0]

On ordering — you asked for the re-raise to come before the per-video failure logging, and
I have done exactly that here. (For contrast, in #1186 I deliberately did the opposite, because
there the per-item handling removes dead sockets from shared state that would otherwise leak. Here
the loop only appends to a function-local results list that is discarded when the function
raises, so there is nothing to preserve and raising first is strictly simpler.)

Regression tests — two added, both proven non-vacuous by removing only the re-raise block and
re-running:

2 failed, 5 passed

E  Failed: DID NOT RAISE CancelledError   (test_cancelled_analysis_is_re_raised_not_treated_as_a_result)
E  Failed: DID NOT RAISE CancelledError   (test_cancellation_takes_precedence_over_ordinary_failures)

The first test uses 4 URLs at batch_size=2 and asserts fake_asyncio.sleep.await_count == 0, so
it also proves the following batch is never dispatched — the concrete symptom you described.
The second covers a cancelled peer alongside an ordinarily-failed one.

Verification at 8725b51d:

tests/unit/test_cloud_ai_routes_batch.py            7 passed
ruff check src/.../cloud_ai_routes.py               Found 7 errors  (same 7 B904 as main — inherited)
ruff check tests/unit/test_cloud_ai_routes_batch.py All checks passed!

I have also corrected the PR body, which previously implied isinstance(result, Exception) was
parity with the old loop. It was not.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Current PR head and relevant changed files =='
git rev-parse --short HEAD
git diff --name-only origin/main...HEAD
git diff --unified=70 origin/main...HEAD -- \
  src/youtube_extension/backend/cloud_ai_routes.py \
  tests/unit/test_cloud_ai_routes_batch.py

echo '== Batch function structure and implementation =='
ast-grep outline src/youtube_extension/backend/cloud_ai_routes.py \
  --items all --match 'process_batch_videos|format_analysis_result' --view expanded
rg -n -C 45 \
  '^async def process_batch_videos\b|asyncio\.gather|batch_results|cancellations|format_analysis_result|await asyncio\.sleep' \
  src/youtube_extension/backend/cloud_ai_routes.py

echo '== Integrator exception behavior =='
integrator="$(fd -t f -i 'integrator.py' src/youtube_extension/integrations/cloud_ai | head -n 1)"
printf 'Integrator: %s\n' "$integrator"
rg -n -C 35 \
  '^class CloudAIIntegrator\b|^    async def analyze_video\b|except (Exception|BaseException|asyncio\.CancelledError)|use_fallback' \
  "$integrator"

echo '== Batch test structure and bodies =='
ast-grep outline tests/unit/test_cloud_ai_routes_batch.py --items all --view expanded
cat -n tests/unit/test_cloud_ai_routes_batch.py

echo '== Existing cancellation handling conventions =='
rg -n -C 5 --glob '*.py' \
  'gather\([^)]*return_exceptions=True|isinstance\([^)]*BaseException|except asyncio\.CancelledError' \
  src tests

Length of output: 48049


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Committed cancellation tests =='
git show HEAD:tests/unit/test_cloud_ai_routes_batch.py | nl -ba | sed -n '115,230p'

echo '== Pytest async configuration =='
for f in pyproject.toml pytest.ini setup.cfg tox.ini; do
  if git cat-file -e "HEAD:$f" 2>/dev/null; then
    echo "-- $f --"
    git show "HEAD:$f" | rg -n -C 3 'asyncio_mode|pytest|asyncio' || true
  fi
done

echo '== Standard-library gather behavior for child cancellation =='
python - <<'PY'
import asyncio

async def cancelled():
    raise asyncio.CancelledError("child cancellation")

async def main():
    result, = await asyncio.gather(cancelled(), return_exceptions=True)
    print(type(result).__module__ + "." + type(result).__qualname__)
    print("is BaseException:", isinstance(result, BaseException))
    print("is Exception:", isinstance(result, Exception))

asyncio.run(main())
PY

echo '== Verify current source catches only Exception at the outer task boundary =='
sed -n '366,438p' src/youtube_extension/backend/cloud_ai_routes.py | nl -ba

Length of output: 1438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Committed cancellation tests =='
git show HEAD:tests/unit/test_cloud_ai_routes_batch.py | sed -n '115,230p'

echo '== Pytest async configuration =='
for f in pyproject.toml pytest.ini setup.cfg tox.ini; do
  if git cat-file -e "HEAD:$f" 2>/dev/null; then
    echo "-- $f --"
    git show "HEAD:$f" | rg -n -C 3 'asyncio_mode|pytest|asyncio' || true
  fi
done

echo '== Current cancellation and outer-handler source =='
sed -n '387,436p' src/youtube_extension/backend/cloud_ai_routes.py

echo '== Test file identity in the commit versus worktree =='
git ls-tree -r --name-only HEAD | rg '^tests/unit/test_cloud_ai_routes_batch\.py$'
test -e tests/unit/test_cloud_ai_routes_batch.py && echo 'worktree test: present' || echo 'worktree test: absent'

Length of output: 9241


@groupthinking The cancellation fix is correct.

process_batch_videos now re-raises gathered BaseException results before it logs ordinary failures or calls format_analysis_result. The outer except Exception does not catch asyncio.CancelledError. Cancellation therefore propagates as it did in the sequential implementation.

The two new tests cover the required cases:

  • A cancelled analysis prevents the next batch and its inter-batch sleep.
  • A cancelled analysis takes precedence over an ordinary per-video failure.

I found no additional production-impacting issue in this change. I could not independently run the new test file because the sandbox worktree does not contain it, although the file is present in commit 8725b51d.

🐇 ✅


✏️ Learnings added
Learnt from: groupthinking
URL: https://github.com/groupthinking/EventRelay/pull/1188

Timestamp: 2026-08-01T22:05:22.208Z
Learning: In `src/youtube_extension/backend/cloud_ai_routes.py`, `process_batch_videos` must re-raise gathered `BaseException` results that are not `Exception` before ordinary per-video failure handling. `asyncio.gather(..., return_exceptions=True)` can return `asyncio.CancelledError` as a result value. The outer handler catches only `Exception`, so this ordering preserves cancellation propagation and prevents `format_analysis_result` from receiving a cancellation object.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

@copilot — this finding is correct, and it was worse than a swallowed cancellation. Thank you.

You are right that gather(..., return_exceptions=True) captures a child's CancelledError as a...

The fix in 8725b51 is correct. The two-step check — collect BaseException-but-not-Exception values, raise the first — restores the propagation semantics of the original await/except Exception loop without conflating ordinary per-video failures with cancellation.

One minor note on the test for test_cancelled_analysis_is_re_raised_not_treated_as_a_result: the additional assertion fake_asyncio.sleep.await_count == 0 is a good concrete check that the following batch was never dispatched, which is the real symptom you described (silently abandoning remaining batches). That makes the test more than just "did it raise" — it pins the blast radius of the bug.

@groupthinking
groupthinking merged commit 8fd1d60 into main Aug 1, 2026
34 of 48 checks passed
@groupthinking
groupthinking deleted the perf/batch-video-analyze branch August 1, 2026 22:21
@linear-code

linear-code Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

GRV-224

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(cloud-ai): process_batch_videos analyses each batch sequentially, making batch_size inert

3 participants