Skip to content

Refs/heads/main#4771

Open
Kiendas25 wants to merge 4 commits into
tinyhumansai:mainfrom
Kiendas25:main
Open

Refs/heads/main#4771
Kiendas25 wants to merge 4 commits into
tinyhumansai:mainfrom
Kiendas25:main

Conversation

@Kiendas25

@Kiendas25 Kiendas25 commented Jul 10, 2026

Copy link
Copy Markdown

Summary

  • What changed and why.
  • Keep this to 3-6 bullets focused on user-visible or architecture-impacting changes.

Problem

  • What issue or risk this PR addresses.
  • Include context needed for reviewers to evaluate correctness quickly.

Solution

  • How the implementation solves the problem.
  • Note important design decisions and tradeoffs.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/ci-lite.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • Coverage matrix updated — added/removed/renamed feature rows in docs/TEST-COVERAGE-MATRIX.md reflect this change (or N/A: behaviour-only change)
  • All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md)
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Runtime/platform impact (desktop/mobile/web/CLI), if any.
  • Performance, security, migration, or compatibility implications.

Related

  • Closes:
  • Follow-up PR(s)/TODOs:

AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key:
  • URL:

Commit & Branch

  • Branch:
  • Commit SHA:

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests:
  • Rust fmt/check (if changed):
  • Tauri fmt/check (if changed):

Validation Blocked

  • command:
  • error:
  • impact:

Behavior Changes

  • Intended behavior change:
  • User-visible effect:

Parity Contract

  • Legacy behavior preserved:
  • Guard/fallback/dispatch parity checks:

Duplicate / Superseded PR Handling

  • Duplicate PR(s):
  • Canonical PR:
  • Resolution (closed/superseded/updated):

Summary by CodeRabbit

  • New Features
    • Added an automated short-video production pipeline covering idea generation, voice synthesis, video editing, subtitles, thumbnails, uploading, and analytics.
    • Added support for scheduled YouTube publishing with configurable privacy, limits, languages, and publishing times.
    • Added configurable content niches, AI providers, voice engines, branding, video settings, and output reporting.
    • Added analytics collection and top-performing video reports in JSON and text formats.
    • Added persistent tracking for scripts, videos, uploads, and analytics.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The pull request adds an end-to-end shorts pipeline with YAML configuration, SQLAlchemy persistence, LLM script generation, voice synthesis, video assembly, YouTube publishing, analytics reporting, and scheduled orchestration.

Shorts pipeline

Layer / File(s) Summary
Configuration and persistence foundation
shorts_pipeline/config.yaml, shorts_pipeline/database/*, shorts_pipeline/requirements.txt
Defines runtime settings, pinned dependencies, session utilities, and ORM models for scripts, videos, and analytics.
Pipeline orchestration
shorts_pipeline/main.py
Loads configuration, initializes logging and database tables, executes pipeline stages, and schedules recurring runs.
Script generation and voice synthesis
shorts_pipeline/modules/idea_generator.py, shorts_pipeline/modules/voice_synthesizer.py
Generates niche-specific scripts through configured LLM providers and produces audio using ElevenLabs with local TTS fallback.
Footage, subtitles, and video assembly
shorts_pipeline/modules/video_assembler.py
Fetches footage, renders subtitles, combines audio and video, re-encodes with NVENC, and marks records as edited.
Publishing and analytics reporting
shorts_pipeline/modules/uploader.py, shorts_pipeline/modules/thumbnail_generator.py, shorts_pipeline/modules/analytics.py
Creates thumbnails, uploads scheduled videos to YouTube, stores upload metadata, captures analytics, and writes JSON/TXT reports.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant Pipeline
  participant LLM
  participant TTS
  participant VideoAssembly
  participant YouTube
  Scheduler->>Pipeline: trigger scheduled run
  Pipeline->>LLM: generate script ideas
  Pipeline->>TTS: synthesize pending scripts
  Pipeline->>VideoAssembly: assemble voiced videos
  Pipeline->>YouTube: upload edited videos
Loading

Poem

I’m a rabbit with scripts in my paws,
Hopping through clips without pauses.
Voices bloom, subtitles shine,
Uploads queue in a publish line.
Analytics carrots grow—
What a marvelous pipeline show!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is just a branch ref and does not describe the changes in the pull request. Replace it with a concise, descriptive title that summarizes the main change, such as adding the autonomous YouTube Shorts pipeline scaffold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

Warning

⚠️ This pull request has been flagged as potential spam (bot-spam) by CodeRabbit slop detection and should be reviewed carefully.

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

This PR introduces a new standalone Python-based “shorts_pipeline” subsystem intended to autonomously generate short-form video content end-to-end (idea generation → voice synthesis → video assembly → YouTube upload → analytics), using a local SQLite database for state tracking.

Changes:

  • Adds a pinned Python dependency set for the pipeline runtime.
  • Implements pipeline modules for LLM idea generation, TTS voice generation (ElevenLabs with local fallback), footage retrieval + video rendering, and YouTube uploading.
  • Adds initial configuration (config.yaml) and a scheduler-driven entrypoint (main.py) to run the pipeline periodically.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
shorts_pipeline/requirements.txt Adds pinned Python dependencies for LLMs, TTS, video editing, YouTube APIs, retries, and DB.
shorts_pipeline/main.py Adds pipeline entrypoint, config loading, logging, DB init, and APScheduler loop.
shorts_pipeline/config.yaml Adds default configuration for AI, niches, voice/video settings, upload, and analytics outputs.
shorts_pipeline/database/db.py Adds SQLite engine/session helpers and a transactional session context manager.
shorts_pipeline/database/models.py Adds SQLAlchemy models for scripts, rendered videos, and analytics metrics.
shorts_pipeline/modules/idea_generator.py Generates script ideas via OpenAI/Anthropic and persists them.
shorts_pipeline/modules/voice_synthesizer.py Generates audio for pending scripts via ElevenLabs with local fallback.
shorts_pipeline/modules/video_assembler.py Fetches stock footage and assembles final videos with subtitles and NVENC encoding.
shorts_pipeline/modules/thumbnail_generator.py Generates a simple branded thumbnail image via Pillow.
shorts_pipeline/modules/uploader.py Uploads edited videos to YouTube (scheduled publishing + thumbnail upload).
shorts_pipeline/modules/analytics.py Fetches basic YouTube Analytics metrics and writes weekly reports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +54 to +56
async def synthesize_pending(self, session):
scripts = session.query(Script).filter(Script.status == "pending").limit(30).all()
await asyncio.gather(*[self._process_script(session, s) for s in scripts], return_exceptions=True)
Comment on lines +59 to +61
video.scheduled_at = video.scheduled_at or self._next_schedule(i)
try:
yid, title, desc, tags, thumb = self._upload_one(video, script)
headers = {"Authorization": self.cfg["video"]["pexels_api_key"]}
r = requests.get("https://api.pexels.com/videos/search", params={"query": query, "per_page": 1}, headers=headers, timeout=30)
r.raise_for_status()
url = r.json()["videos"][0]["video_files"][0]["link"]
r.raise_for_status()
url = r.json()["videos"][0]["video_files"][0]["link"]
out = Path("output/footage") / f"{script_id}.mp4"
out.write_bytes(requests.get(url, timeout=60).content)
Comment thread shorts_pipeline/main.py
Comment on lines +5 to +8
3) Fill config.yaml keys: OpenAI/Anthropic, ElevenLabs, Pexels/Pixabay, YouTube OAuth files.
4) Place branding assets under assets/branding and font under assets/fonts.
5) Run once: python main.py (it bootstraps DB and schedules jobs).
6) Keep this process alive (systemd/supervisor/docker). It runs autonomous loops.
Comment on lines +44 to +54
for row in scripts:
session.add(
Script(
niche=niche,
idea=row["idea"],
hook=row["hook"],
content=row["content"],
retention_hook=row["retention_hook"],
status="pending",
)
)

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 10

🧹 Nitpick comments (4)
shorts_pipeline/modules/voice_synthesizer.py (1)

54-56: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

return_exceptions=True silently drops per-script failures.

Exceptions from _process_script (e.g., both ElevenLabs and local TTS failing) are captured but never inspected, so failed scripts stay pending with no log trail. Iterate the gathered results and log exceptions.

♻️ Log gathered failures
-        await asyncio.gather(*[self._process_script(session, s) for s in scripts], return_exceptions=True)
+        results = await asyncio.gather(*[self._process_script(session, s) for s in scripts], return_exceptions=True)
+        for script, result in zip(scripts, results):
+            if isinstance(result, Exception):
+                LOGGER.error("Synthesis failed for script=%s: %s", script.id, result)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/voice_synthesizer.py` around lines 54 - 56, Inspect
the results returned by asyncio.gather in synthesize_pending instead of
discarding them: iterate over each result, identify exceptions from
_process_script, and log them with the affected script context so failures are
traceable while preserving processing of other scripts.
shorts_pipeline/modules/video_assembler.py (1)

26-31: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Empty Pexels results raise IndexError and get retried 5× pointlessly; download lacks raise_for_status().

r.json()["videos"][0]... raises IndexError when a query returns no videos, and because it's inside the tenacity.retry(5) policy the same empty query is re-issued five times before failing. Guard the empty case (and skip retrying it). The follow-up requests.get(url, timeout=60) also omits raise_for_status(), so an error response body could be written as the .mp4.

♻️ Guard empty results and validate the download
         r.raise_for_status()
-        url = r.json()["videos"][0]["video_files"][0]["link"]
+        videos = r.json().get("videos") or []
+        if not videos:
+            raise ValueError(f"No Pexels footage for query={query!r}")
+        url = videos[0]["video_files"][0]["link"]
         out = Path("output/footage") / f"{script_id}.mp4"
-        out.write_bytes(requests.get(url, timeout=60).content)
+        dl = requests.get(url, timeout=60)
+        dl.raise_for_status()
+        out.write_bytes(dl.content)
         return out
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/video_assembler.py` around lines 26 - 31, Handle
empty Pexels search results explicitly in the footage-fetching function before
indexing the videos array, raising a non-retryable error or otherwise skipping
retry for this expected condition. Add raise_for_status() to the follow-up
download response before writing its content, using the existing request/retry
logic and function symbols to preserve retries only for transient failures.
shorts_pipeline/modules/idea_generator.py (1)

21-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Anthropic model is hardcoded while OpenAI uses config["ai"]["model"].

The provider branches are inconsistent: claude-3-7-sonnet-latest is baked in, but OpenAI reads the model from config. Consider sourcing both from config so the model is configurable per provider.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/idea_generator.py` around lines 21 - 29, The
_llm_call method hardcodes the Anthropic model instead of using configuration.
Read the Anthropic model from the appropriate ai_cfg setting, matching the
configurable model behavior already used by the OpenAI branch, while preserving
the existing provider-specific API calls.
shorts_pipeline/config.yaml (1)

9-10: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use environment variables for API keys instead of committed config values.

The config defines openai_api_key, anthropic_api_key, elevenlabs_api_key, and pexels_api_key as empty strings. While safe as a template, this pattern encourages filling them in and accidentally committing secrets. Consider having load_config override empty values from environment variables (e.g., OPENAI_API_KEY, ELEVENLABS_API_KEY, PEXELS_API_KEY).

🔐 Suggested env-var override in load_config
 def load_config():
    with open("config.yaml", "r", encoding="utf-8") as f:
        return yaml.safe_load(f)
# In main.py load_config():
import os

ENV_OVERRIDES = {
    ("ai", "openai_api_key"): "OPENAI_API_KEY",
    ("ai", "anthropic_api_key"): "ANTHROPIC_API_KEY",
    ("voice", "elevenlabs_api_key"): "ELEVENLABS_API_KEY",
    ("video", "pexels_api_key"): "PEXELS_API_KEY",
    ("video", "pixabay_api_key"): "PIXABAY_API_KEY",
}

def load_config():
    with open("config.yaml", "r", encoding="utf-8") as f:
        cfg = yaml.safe_load(f)
    for (section, key), env_name in ENV_OVERRIDES.items():
        env_val = os.environ.get(env_name)
        if env_val:
            cfg[section][key] = env_val
    return cfg

Also applies to: 24-24, 32-32

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/config.yaml` around lines 9 - 10, Update load_config in
main.py to override all API-key settings with non-empty environment variables:
OPENAI_API_KEY, ANTHROPIC_API_KEY, ELEVENLABS_API_KEY, PEXELS_API_KEY, and
PIXABAY_API_KEY. Add the required os import and map each environment variable to
its config section/key, preserving YAML values when variables are unset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shorts_pipeline/main.py`:
- Around line 48-52: Shared SQLAlchemy sessions across concurrent
voice-processing coroutines can cause autoflush conflicts and swallowed
failures. Update the caller around VoiceSynthesizer.synthesize_pending to pass
the session factory instead of a live session, then change _process_script to
create its own session_scope, merge the Script into it, update or create the
Video, and commit within that context; alternatively make synthesize_pending
process scripts sequentially using the existing session.
- Around line 36-38: Update load_config to resolve config.yaml relative to the
project/script location rather than the current working directory, following
db.py’s Path(__file__).resolve().parents[1] pattern; use the resolved path in
open while preserving UTF-8 reading and yaml.safe_load behavior.

In `@shorts_pipeline/modules/analytics.py`:
- Around line 38-44: _write_report lacks creation of parent directories before
opening its JSON and text report files. In _write_report, derive each configured
output path’s parent directory and create it with parents enabled and
exist_ok=True before the corresponding open calls, matching the behavior used by
thumbnail_generator.py.
- Around line 26-27: Handle empty analytics results safely in the YouTube query
logic: update row extraction in the method containing the reports().query call
to default to [0, 0.0, 0.0] when rows is absent or empty, such as by using a
fallback after retrieving resp.get("rows"). Ensure videos with no analytics data
do not raise IndexError or remain endlessly retried as "uploaded".

In `@shorts_pipeline/modules/idea_generator.py`:
- Around line 38-55: Handle malformed script rows within each niche in
generate_and_store: move row validation and Script creation into per-row error
handling, or otherwise keep failures from escaping the niche loop. Ensure
invalid rows are logged and skipped while valid rows are stored and subsequent
niches continue processing; update the logic around _llm_call, json parsing, and
Script creation accordingly.

In `@shorts_pipeline/modules/uploader.py`:
- Around line 26-32: Update the credential handling around
Credentials.from_authorized_user_file in the uploader authentication logic to
distinguish missing or invalid token files, refresh existing expired credentials
when a refresh token is available, and avoid unbounded run_local_server
interactive authentication in headless pipelines. Use a bounded/non-interactive
fallback or explicitly fail with a clear error when unattended credentials are
unavailable; do not catch all exceptions indiscriminately.
- Around line 56-72: Update upload_due so the queue query orders results
deterministically with order_by(Video.id) before limit/all, and replace
datetime.utcnow() with datetime.now(timezone.utc) when setting uploaded_at;
ensure the timezone import is available and consistent with _next_schedule.

In `@shorts_pipeline/modules/video_assembler.py`:
- Around line 38-56: Ensure the rendering loop always releases MoviePy resources
and removes temporary output on failure. In the block identified by
VideoFileClip, AudioFileClip, and CompositeVideoClip, initialize clip references
safely, close final, sub, vc, and ac in a finally block, and unlink tmp with
missing_ok=True there; avoid relying on the success path cleanup while
preserving the existing exception logging.

In `@shorts_pipeline/modules/voice_synthesizer.py`:
- Around line 39-47: The synchronous local fallback in _process_script blocks
the event loop and recreates the TTS model on every failure. Run _local_tts via
asyncio.to_thread, and refactor its model usage to reuse a shared or lazily
initialized TTS instance where possible.

In `@shorts_pipeline/requirements.txt`:
- Line 1: Update the dependency pins in requirements.txt: change aiohttp from
3.11.18 to at least 3.14.1 and Pillow from 10.4.0 to at least 12.2.0, preserving
the existing exact-pin format.

---

Nitpick comments:
In `@shorts_pipeline/config.yaml`:
- Around line 9-10: Update load_config in main.py to override all API-key
settings with non-empty environment variables: OPENAI_API_KEY,
ANTHROPIC_API_KEY, ELEVENLABS_API_KEY, PEXELS_API_KEY, and PIXABAY_API_KEY. Add
the required os import and map each environment variable to its config
section/key, preserving YAML values when variables are unset.

In `@shorts_pipeline/modules/idea_generator.py`:
- Around line 21-29: The _llm_call method hardcodes the Anthropic model instead
of using configuration. Read the Anthropic model from the appropriate ai_cfg
setting, matching the configurable model behavior already used by the OpenAI
branch, while preserving the existing provider-specific API calls.

In `@shorts_pipeline/modules/video_assembler.py`:
- Around line 26-31: Handle empty Pexels search results explicitly in the
footage-fetching function before indexing the videos array, raising a
non-retryable error or otherwise skipping retry for this expected condition. Add
raise_for_status() to the follow-up download response before writing its
content, using the existing request/retry logic and function symbols to preserve
retries only for transient failures.

In `@shorts_pipeline/modules/voice_synthesizer.py`:
- Around line 54-56: Inspect the results returned by asyncio.gather in
synthesize_pending instead of discarding them: iterate over each result,
identify exceptions from _process_script, and log them with the affected script
context so failures are traceable while preserving processing of other scripts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 36ad0745-8bed-4df4-936c-fe5119bd3ff1

📥 Commits

Reviewing files that changed from the base of the PR and between f5b77ed and 96fa5c0.

📒 Files selected for processing (11)
  • shorts_pipeline/config.yaml
  • shorts_pipeline/database/db.py
  • shorts_pipeline/database/models.py
  • shorts_pipeline/main.py
  • shorts_pipeline/modules/analytics.py
  • shorts_pipeline/modules/idea_generator.py
  • shorts_pipeline/modules/thumbnail_generator.py
  • shorts_pipeline/modules/uploader.py
  • shorts_pipeline/modules/video_assembler.py
  • shorts_pipeline/modules/voice_synthesizer.py
  • shorts_pipeline/requirements.txt

Comment thread shorts_pipeline/main.py
Comment on lines +36 to +38
def load_config():
with open("config.yaml", "r", encoding="utf-8") as f:
return yaml.safe_load(f)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

load_config uses a relative path — will break if run from another directory.

open("config.yaml") resolves against the current working directory, not the script location. Running python shorts_pipeline/main.py from the repo root will fail with FileNotFoundError. db.py already uses Path(__file__).resolve().parents[1] for absolute paths — load_config should follow the same pattern.

🛠️ Proposed fix
 def load_config():
-    with open("config.yaml", "r", encoding="utf-8") as f:
+    config_path = Path(__file__).parent / "config.yaml"
+    with open(config_path, "r", encoding="utf-8") as f:
         return yaml.safe_load(f)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def load_config():
with open("config.yaml", "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def load_config():
config_path = Path(__file__).parent / "config.yaml"
with open(config_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/main.py` around lines 36 - 38, Update load_config to resolve
config.yaml relative to the project/script location rather than the current
working directory, following db.py’s Path(__file__).resolve().parents[1]
pattern; use the resolved path in open while preserving UTF-8 reading and
yaml.safe_load behavior.

Comment thread shorts_pipeline/main.py
Comment on lines +48 to +52
with session_scope(sf) as session:
try:
asyncio.run(VoiceSynthesizer(cfg).synthesize_pending(session))
except Exception:
logger.exception("Voice step failed")

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Shared SQLAlchemy session across concurrent async coroutines risks silent data loss.

The session from session_scope is passed to VoiceSynthesizer.synthesize_pending, which dispatches multiple _process_script coroutines via asyncio.gather(..., return_exceptions=True). Each coroutine calls session.query(Video)... and session.add(video). SQLAlchemy's auto-flush can flush one coroutine's pending changes during another's query; if that flush fails, the session enters a PendingRollbackError state and all subsequent coroutines fail silently (swallowed by return_exceptions=True).

Consider passing the session factory instead and giving each coroutine its own session, or processing scripts sequentially within the single session.

🔧 Suggested approach: per-coroutine sessions

In main.py, pass the factory instead of the session to the voice step:

-    with session_scope(sf) as session:
-        try:
-            asyncio.run(VoiceSynthesizer(cfg).synthesize_pending(session))
-        except Exception:
-            logger.exception("Voice step failed")
+    try:
+        asyncio.run(VoiceSynthesizer(cfg).synthesize_pending(sf))
+    except Exception:
+        logger.exception("Voice step failed")

Then in voice_synthesizer.py, each _process_script creates and commits its own session:

async def _process_script(self, sf, script: Script):
    # ... elevenlabs/local tts ...
    with session_scope(sf) as session:
        script = session.merge(script)  # reattach to this session
        script.status = "voiced"
        video = session.query(Video).filter_by(script_id=script.id).one_or_none() or Video(script_id=script.id)
        video.audio_path = str(audio_path)
        video.status = "voiced"
        session.add(video)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/main.py` around lines 48 - 52, Shared SQLAlchemy sessions
across concurrent voice-processing coroutines can cause autoflush conflicts and
swallowed failures. Update the caller around VoiceSynthesizer.synthesize_pending
to pass the session factory instead of a live session, then change
_process_script to create its own session_scope, merge the Script into it,
update or create the Video, and commit within that context; alternatively make
synthesize_pending process scripts sequentially using the existing session.

Comment on lines +26 to +27
resp = self.api.reports().query(ids="channel==MINE", startDate=(video.uploaded_at.date()).isoformat(), endDate=datetime.utcnow().date().isoformat(), metrics="views,averageViewDuration,averageViewPercentage", filters=f"video=={video.youtube_video_id}").execute()
vals = resp.get("rows", [[0, 0.0, 0.0]])[0]

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

IndexError when YouTube Analytics API returns empty rows

resp.get("rows", [[0, 0.0, 0.0]])[0] only defaults when the rows key is absent. If the API returns {"rows": []} (common for videos with no analytics data yet), get returns [] and [][0] raises IndexError. The exception is caught on Line 33, but the video stays in "uploaded" status and will be retried on every subsequent run, producing repeated log errors.

🐛 Proposed fix: safe row extraction
-                vals = resp.get("rows", [[0, 0.0, 0.0]])[0]
+                rows = resp.get("rows", [])
+                vals = rows[0] if rows else [0, 0.0, 0.0]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resp = self.api.reports().query(ids="channel==MINE", startDate=(video.uploaded_at.date()).isoformat(), endDate=datetime.utcnow().date().isoformat(), metrics="views,averageViewDuration,averageViewPercentage", filters=f"video=={video.youtube_video_id}").execute()
vals = resp.get("rows", [[0, 0.0, 0.0]])[0]
resp = self.api.reports().query(ids="channel==MINE", startDate=(video.uploaded_at.date()).isoformat(), endDate=datetime.utcnow().date().isoformat(), metrics="views,averageViewDuration,averageViewPercentage", filters=f"video=={video.youtube_video_id}").execute()
rows = resp.get("rows", [])
vals = rows[0] if rows else [0, 0.0, 0.0]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/analytics.py` around lines 26 - 27, Handle empty
analytics results safely in the YouTube query logic: update row extraction in
the method containing the reports().query call to default to [0, 0.0, 0.0] when
rows is absent or empty, such as by using a fallback after retrieving
resp.get("rows"). Ensure videos with no analytics data do not raise IndexError
or remain endlessly retried as "uploaded".

Comment on lines +38 to +44
def _write_report(self, scored: list[dict]):
with open(self.cfg["analytics"]["report_output_json"], "w", encoding="utf-8") as f:
json.dump({"generated_at": datetime.utcnow().isoformat(), "top_videos": scored[:20]}, f, ensure_ascii=False, indent=2)
with open(self.cfg["analytics"]["report_output_txt"], "w", encoding="utf-8") as f:
f.write("Weekly Shorts Performance Report\n\n")
for i, row in enumerate(scored[:20], 1):
f.write(f"{i}. Script {row['script_id']} | {row['niche']} | Views={row['views']} | Retention={row['retention_rate']:.2f}\n")

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Missing parent directory creation before writing report files

Unlike thumbnail_generator.py (Line 14), _write_report does not create parent directories before opening files for writing. If the output directory doesn't exist, open() will raise FileNotFoundError.

🛡️ Proposed fix
     def _write_report(self, scored: list[dict]):
+        from pathlib import Path
+        Path(self.cfg["analytics"]["report_output_json"]).parent.mkdir(parents=True, exist_ok=True)
+        Path(self.cfg["analytics"]["report_output_txt"]).parent.mkdir(parents=True, exist_ok=True)
         with open(self.cfg["analytics"]["report_output_json"], "w", encoding="utf-8") as f:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _write_report(self, scored: list[dict]):
with open(self.cfg["analytics"]["report_output_json"], "w", encoding="utf-8") as f:
json.dump({"generated_at": datetime.utcnow().isoformat(), "top_videos": scored[:20]}, f, ensure_ascii=False, indent=2)
with open(self.cfg["analytics"]["report_output_txt"], "w", encoding="utf-8") as f:
f.write("Weekly Shorts Performance Report\n\n")
for i, row in enumerate(scored[:20], 1):
f.write(f"{i}. Script {row['script_id']} | {row['niche']} | Views={row['views']} | Retention={row['retention_rate']:.2f}\n")
def _write_report(self, scored: list[dict]):
from pathlib import Path
Path(self.cfg["analytics"]["report_output_json"]).parent.mkdir(parents=True, exist_ok=True)
Path(self.cfg["analytics"]["report_output_txt"]).parent.mkdir(parents=True, exist_ok=True)
with open(self.cfg["analytics"]["report_output_json"], "w", encoding="utf-8") as f:
json.dump({"generated_at": datetime.utcnow().isoformat(), "top_videos": scored[:20]}, f, ensure_ascii=False, indent=2)
with open(self.cfg["analytics"]["report_output_txt"], "w", encoding="utf-8") as f:
f.write("Weekly Shorts Performance Report\n\n")
for i, row in enumerate(scored[:20], 1):
f.write(f"{i}. Script {row['script_id']} | {row['niche']} | Views={row['views']} | Retention={row['retention_rate']:.2f}\n")
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 38-38: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(self.cfg["analytics"]["report_output_json"], "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 40-40: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(self.cfg["analytics"]["report_output_txt"], "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/analytics.py` around lines 38 - 44, _write_report
lacks creation of parent directories before opening its JSON and text report
files. In _write_report, derive each configured output path’s parent directory
and create it with parents enabled and exist_ok=True before the corresponding
open calls, matching the behavior used by thumbnail_generator.py.

Comment on lines +38 to +55
try:
raw = self._llm_call(prompt)
scripts = json.loads(raw[raw.find("[") : raw.rfind("]") + 1])
except Exception as exc:
LOGGER.exception("Failed generating scripts for niche=%s: %s", niche, exc)
continue
for row in scripts:
session.add(
Script(
niche=niche,
idea=row["idea"],
hook=row["hook"],
content=row["content"],
retention_hook=row["retention_hook"],
status="pending",
)
)
LOGGER.info("Stored %s scripts for niche=%s", len(scripts), niche)

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Handle malformed LLM rows per niche
row["idea"]/row["hook"] are outside the try, so one bad object stops the rest of generate_and_store() and skips later niches in that run. Validate each row or keep the per-row add inside the niche-level error handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/idea_generator.py` around lines 38 - 55, Handle
malformed script rows within each niche in generate_and_store: move row
validation and Script creation into per-row error handling, or otherwise keep
failures from escaping the niche loop. Ensure invalid rows are logged and
skipped while valid rows are stored and subsequent niches continue processing;
update the logic around _llm_call, json parsing, and Script creation
accordingly.

Comment on lines +26 to +32
try:
creds = Credentials.from_authorized_user_file(token_file, SCOPES)
except Exception:
flow = InstalledAppFlow.from_client_secrets_file(self.cfg["upload"]["youtube_client_secrets_file"], SCOPES)
creds = flow.run_local_server(port=0)
with open(token_file, "w", encoding="utf-8") as f:
f.write(creds.to_json())

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

run_local_server blocks indefinitely in headless/autonomous environments

The OAuth fallback starts a local HTTP server (port=0) requiring browser interaction. In an autonomous pipeline running headless, this hangs forever if the token file is missing, corrupted, or the refresh token is expired. There is also no explicit token-refresh path before falling back — the broad except Exception on Line 28 masks the difference between "file not found" and "credentials invalid."

Consider adding a token-refresh step before falling back to interactive auth, or switch to a service-account flow for unattended operation.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 30-30: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(token_file, "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.15.20)

[warning] 28-28: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/uploader.py` around lines 26 - 32, Update the
credential handling around Credentials.from_authorized_user_file in the uploader
authentication logic to distinguish missing or invalid token files, refresh
existing expired credentials when a refresh token is available, and avoid
unbounded run_local_server interactive authentication in headless pipelines. Use
a bounded/non-interactive fallback or explicitly fail with a clear error when
unattended credentials are unavailable; do not catch all exceptions
indiscriminately.

Comment on lines +56 to +72
def upload_due(self, session):
queue = session.query(Video, Script).join(Script, Video.script_id == Script.id).filter(Video.status == "edited").limit(self.cfg["upload"]["max_videos_per_day"]).all()
for i, (video, script) in enumerate(queue):
video.scheduled_at = video.scheduled_at or self._next_schedule(i)
try:
yid, title, desc, tags, thumb = self._upload_one(video, script)
video.youtube_video_id = yid
video.title = title
video.description = desc
video.tags = ",".join(tags)
video.thumbnail_path = thumb
video.uploaded_at = datetime.utcnow()
video.status = "uploaded"
script.status = "uploaded"
LOGGER.info("Uploaded script=%s youtube_id=%s", script.id, yid)
except Exception as exc:
LOGGER.exception("Upload failed for script=%s err=%s", script.id, exc)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing order_by and naive datetime inconsistency in upload_due

Two issues in this method:

  1. Line 57 — The query lacks .order_by(), making the scheduling order non-deterministic across runs. The _next_schedule(i) call assigns publish times based on index i, so the same video could get different schedule slots between runs. Add .order_by(Video.id) for deterministic scheduling.

  2. Line 67datetime.utcnow() returns a naive datetime, while _next_schedule (Line 36) returns a timezone-aware datetime (datetime.now(timezone.utc)). This creates a mismatch between scheduled_at (aware) and uploaded_at (naive) in the same table. If any downstream code compares these fields, it will raise TypeError. Use datetime.now(timezone.utc) consistently.

🛡️ Proposed fixes
-        queue = session.query(Video, Script).join(Script, Video.script_id == Script.id).filter(Video.status == "edited").limit(self.cfg["upload"]["max_videos_per_day"]).all()
+        queue = session.query(Video, Script).join(Script, Video.script_id == Script.id).filter(Video.status == "edited").order_by(Video.id).limit(self.cfg["upload"]["max_videos_per_day"]).all()
-                video.uploaded_at = datetime.utcnow()
+                video.uploaded_at = datetime.now(timezone.utc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def upload_due(self, session):
queue = session.query(Video, Script).join(Script, Video.script_id == Script.id).filter(Video.status == "edited").limit(self.cfg["upload"]["max_videos_per_day"]).all()
for i, (video, script) in enumerate(queue):
video.scheduled_at = video.scheduled_at or self._next_schedule(i)
try:
yid, title, desc, tags, thumb = self._upload_one(video, script)
video.youtube_video_id = yid
video.title = title
video.description = desc
video.tags = ",".join(tags)
video.thumbnail_path = thumb
video.uploaded_at = datetime.utcnow()
video.status = "uploaded"
script.status = "uploaded"
LOGGER.info("Uploaded script=%s youtube_id=%s", script.id, yid)
except Exception as exc:
LOGGER.exception("Upload failed for script=%s err=%s", script.id, exc)
def upload_due(self, session):
queue = session.query(Video, Script).join(Script, Video.script_id == Script.id).filter(Video.status == "edited").order_by(Video.id).limit(self.cfg["upload"]["max_videos_per_day"]).all()
for i, (video, script) in enumerate(queue):
video.scheduled_at = video.scheduled_at or self._next_schedule(i)
try:
yid, title, desc, tags, thumb = self._upload_one(video, script)
video.youtube_video_id = yid
video.title = title
video.description = desc
video.tags = ",".join(tags)
video.thumbnail_path = thumb
video.uploaded_at = datetime.now(timezone.utc)
video.status = "uploaded"
script.status = "uploaded"
LOGGER.info("Uploaded script=%s youtube_id=%s", script.id, yid)
except Exception as exc:
LOGGER.exception("Upload failed for script=%s err=%s", script.id, exc)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/uploader.py` around lines 56 - 72, Update upload_due
so the queue query orders results deterministically with order_by(Video.id)
before limit/all, and replace datetime.utcnow() with datetime.now(timezone.utc)
when setting uploaded_at; ensure the timezone import is available and consistent
with _next_schedule.

Comment on lines +38 to +56
for video, script in rows:
try:
footage = self._fetch_footage(script.idea, script.id)
vc = VideoFileClip(str(footage)).resize((self.cfg["video"]["width"], self.cfg["video"]["height"]))
ac = AudioFileClip(video.audio_path)
clip = vc.subclip(0, min(vc.duration, ac.duration + 1)).set_audio(ac)
sub = self._subtitle_clip(f"{script.hook} {script.retention_hook}", clip.duration)
final = CompositeVideoClip([clip, sub])
out = Path("output/videos") / f"video_{script.id}.mp4"
tmp = out.with_suffix(".tmp.mp4")
final.write_videofile(str(tmp), fps=self.cfg["video"]["fps"], codec="libx264", audio_codec="aac", logger=None)
subprocess.run(["ffmpeg", "-y", "-i", str(tmp), "-c:v", "h264_nvenc", "-preset", self.cfg["video"]["nvenc_preset"], "-c:a", "aac", str(out)], check=True)
tmp.unlink(missing_ok=True)
video.video_path = str(out)
video.status = "edited"
script.status = "edited"
LOGGER.info("Rendered %s", out)
except Exception as exc:
LOGGER.exception("Video render failed script=%s error=%s", script.id, exc)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and nearby symbols first.
ast-grep outline shorts_pipeline/modules/video_assembler.py --view expanded || true

printf '\n--- file with line numbers ---\n'
cat -n shorts_pipeline/modules/video_assembler.py | sed -n '1,260p'

printf '\n--- search for close()/with usage in related modules ---\n'
rg -n "\.close\(\)|with VideoFileClip|with AudioFileClip|with CompositeVideoClip|write_videofile|ffmpeg" shorts_pipeline -g '*.py'

Repository: tinyhumansai/openhuman

Length of output: 4396


Close the MoviePy clips in a finally.
VideoFileClip, AudioFileClip, and CompositeVideoClip stay open here, and tmp is only removed on the success path. Close the clips and clean up the temp file even when rendering fails.

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 48-48: Command coming from incoming request
Context: subprocess.run(["ffmpeg", "-y", "-i", str(tmp), "-c:v", "h264_nvenc", "-preset", self.cfg["video"]["nvenc_preset"], "-c:a", "aac", str(out)], check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.20)

[error] 49-49: subprocess call: check for execution of untrusted input

(S603)


[error] 49-49: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/video_assembler.py` around lines 38 - 56, Ensure the
rendering loop always releases MoviePy resources and removes temporary output on
failure. In the block identified by VideoFileClip, AudioFileClip, and
CompositeVideoClip, initialize clip references safely, close final, sub, vc, and
ac in a finally block, and unlink tmp with missing_ok=True there; avoid relying
on the success path cleanup while preserving the existing exception logging.

Comment on lines +39 to +47
async def _process_script(self, session, script: Script):
text = f"{script.hook}. {script.content}. {script.retention_hook}."
audio_path = self.out_dir / f"script_{script.id}.mp3"
try:
async with self.sem:
await self._elevenlabs(text, audio_path)
except Exception as exc:
LOGGER.warning("ElevenLabs failed for script=%s. Falling back local TTS. error=%s", script.id, exc)
self._local_tts(text, audio_path)

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file with line numbers and surrounding context
git ls-files shorts_pipeline/modules/voice_synthesizer.py
wc -l shorts_pipeline/modules/voice_synthesizer.py
sed -n '1,220p' shorts_pipeline/modules/voice_synthesizer.py | cat -n

# Find call sites and task orchestration around synthesize_pending
rg -n "synthesize_pending|_process_script|_local_tts|_elevenlabs|asyncio.gather|to_thread|run_in_executor" shorts_pipeline -S

Repository: tinyhumansai/openhuman

Length of output: 4002


Move the local TTS fallback off the event loop. _local_tts() is synchronous and creates a new TTS(...) instance on each fallback, so an ElevenLabs failure will block the rest of synthesize_pending() until that synthesis finishes. Use asyncio.to_thread(...) here and reuse the model if possible.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 45-45: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/modules/voice_synthesizer.py` around lines 39 - 47, The
synchronous local fallback in _process_script blocks the event loop and
recreates the TTS model on every failure. Run _local_tts via asyncio.to_thread,
and refactor its model usage to reuse a shared or lazily initialized TTS
instance where possible.

@@ -0,0 +1,18 @@
aiohttp==3.11.18

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest patched versions for aiohttp and Pillow
curl -s https://pypi.org/pypi/aiohttp/json | jq '.info.version'
curl -s https://pypi.org/pypi/Pillow/json | jq '.info.version'

# Check for security advisories
gh api graphql -f query='
{
  securityVulnerabilities(first: 10, ecosystem: PIP, package: "aiohttp") {
    nodes {
      advisory { summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'
gh api graphql -f query='
{
  securityVulnerabilities(first: 10, ecosystem: PIP, package: "Pillow") {
    nodes {
      advisory { summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'

Repository: tinyhumansai/openhuman

Length of output: 4937


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files shorts_pipeline/requirements.txt
echo "---"
cat -n shorts_pipeline/requirements.txt
echo "---"
python3 - <<'PY'
from pathlib import Path
p = Path("shorts_pipeline/requirements.txt")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if line.strip() and not line.lstrip().startswith("#"):
        print(f"{i}: {line}")
PY

Repository: tinyhumansai/openhuman

Length of output: 1038


Bump aiohttp and Pillow to patched releases. aiohttp==3.11.18 is below 3.14.1, and Pillow==10.4.0 is below 12.2.0; both pins are in published vulnerability ranges.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[HIGH] 1-1: pillow 10.4.0: undefined

(PYSEC-2026-165)


[HIGH] 1-1: pillow 10.4.0: Pillow affected by out-of-bounds write when loading PSD images

(GHSA-cfh3-3jmp-rvhc)


[HIGH] 1-1: pillow 10.4.0: Pillow has an OOB Write with Invalid PSD Tile Extents (Integer Overflow)

(GHSA-pwv6-vv43-88gr)


[HIGH] 1-1: pillow 10.4.0: Pillow has a PDF Parsing Trailer Infinite Loop (DoS)

(GHSA-r73j-pqj5-w3x7)


[HIGH] 1-1: pillow 10.4.0: FITS GZIP decompression bomb in Pillow

(GHSA-whj4-6x5x-4v2j)


[HIGH] 1-1: pillow 10.4.0: Pillow has an integer overflow when processing fonts

(GHSA-wjx4-4jcj-g98j)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to brute-force leak of internal static file path components

(PYSEC-2026-1097)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP's unicode processing of header values could cause parsing discrepancies

(PYSEC-2026-1099)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to denial of service through large payloads

(PYSEC-2026-1100)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP's HTTP Parser auto_decompress feature is vulnerable to zip bomb

(PYSEC-2026-1101)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP is vulnerable to HTTP Request/Response Smuggling through incorrect parsing of chunked trailer sections

(PYSEC-2026-1104)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP Vulnerable to Cookie Parser Warning Storm

(PYSEC-2026-1105)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to DoS through chunked messages

(PYSEC-2026-1106)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to DoS when bypassing asserts

(PYSEC-2026-1107)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has unicode match groups in regexes for ASCII protocol elements

(PYSEC-2026-1109)


[CRITICAL] 1-1: aiohttp 3.11.18: undefined

(PYSEC-2026-237)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: Host-Only Cookies Become Domain Cookies After CookieJar Persistence

(GHSA-2fqr-mr3j-6wp8)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has CRLF injection through multipart part content type header construction

(GHSA-2vrm-gr82-f7m5)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has late size enforcement for non-file multipart fields causes memory DoS

(GHSA-3wq7-rqq7-wx6j)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: HTTP/1 Pipelined Requests Queue Without Limit

(GHSA-4fvr-rgm6-gqmc)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: TLS Server Hostname Override Is Ignored When Reusing HTTPS Connections

(GHSA-4m7w-qmgq-4wj5)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to brute-force leak of internal static file path components

(GHSA-54jq-c3m8-4m76)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP's C parser (llhttp) accepts null bytes and control characters in response header values - header injection/security bypass

(GHSA-63hf-3vf5-4wqf)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: C HTTP Parser Bypasses max_line_size for Fragmented Lines

(GHSA-63hw-fmq6-xxg2)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP's unicode processing of header values could cause parsing discrepancies

(GHSA-69f9-5gxw-wvc2)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to denial of service through large payloads

(GHSA-6jhg-hg63-jvvf)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP's HTTP Parser auto_decompress feature is vulnerable to zip bomb

(GHSA-6mq8-rvhq-8wgg)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP is vulnerable to HTTP Request/Response Smuggling through incorrect parsing of chunked trailer sections

(GHSA-9548-qrrj-x5pj)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP leaks Cookie and Proxy-Authorization headers on cross-origin redirect

(GHSA-966j-vmvw-g2g9)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: Payload Response Resources Are Not Closed After Mid-Body Disconnect

(GHSA-9x8q-7h8h-wcw9)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP accepts duplicate Host headers

(GHSA-c427-h43c-vf67)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP Vulnerable to Cookie Parser Warning Storm

(GHSA-fh55-r93g-j68g)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: Unread Compressed Request Bodies Bypass client_max_size During Cleanup

(GHSA-g3cq-j2xw-wf74)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to DoS through chunked messages

(GHSA-g84x-mcqj-x9qq)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP Affected by Denial of Service (DoS) via Unbounded DNS Cache in TCPConnector

(GHSA-hcc4-c3v8-rx92)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP is vulnerable to cross-origin redirect with per-request cookies

(GHSA-hg6j-4rv6-33pg)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: DigestAuthMiddleware Applies Credentials to Cross-Origin Redirect Challenges

(GHSA-hpj7-wq8m-9hgp)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP is Vulnerable to Deserialization of Untrusted Data

(GHSA-jg22-mg44-37j8)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to DoS when bypassing asserts

(GHSA-jj3x-wxrx-4x23)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has a Multipart Header Size Bypass

(GHSA-m5qp-6w8w-w647)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: CRLF injection in multipart headers

(GHSA-m6qw-4cw2-hm4m)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has unicode match groups in regexes for ASCII protocol elements

(GHSA-mqqc-3gqh-h2x8)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has HTTP response splitting via \r in reason phrase

(GHSA-mwh4-6h8g-pg8w)


[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP affected by UNC SSRF/NTLMv2 Credential Theft/Local File Read in static resource handler on Windows

(GHSA-p998-jp59-783m)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp allows unlimited trailer headers, leading to possible uncapped memory usage

(GHSA-w2fm-2cpv-w7v5)


[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: Incomplete websocket frame payloads bypass memory limits

(GHSA-xcgm-r5h9-7989)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shorts_pipeline/requirements.txt` at line 1, Update the dependency pins in
requirements.txt: change aiohttp from 3.11.18 to at least 3.14.1 and Pillow from
10.4.0 to at least 12.2.0, preserving the existing exact-pin format.

Source: Linters/SAST tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants