Refs/heads/main#4771
Conversation
…orts-pipeline-in-python Add autonomous YouTube Shorts pipeline project scaffold
📝 WalkthroughWalkthroughChangesThe 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
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
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
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. Comment Warning |
There was a problem hiding this comment.
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.
| 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) |
| 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) |
| 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. |
| 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", | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (4)
shorts_pipeline/modules/voice_synthesizer.py (1)
54-56: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
return_exceptions=Truesilently drops per-script failures.Exceptions from
_process_script(e.g., both ElevenLabs and local TTS failing) are captured but never inspected, so failed scripts staypendingwith 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 winEmpty Pexels results raise
IndexErrorand get retried 5× pointlessly; download lacksraise_for_status().
r.json()["videos"][0]...raisesIndexErrorwhen a query returns no videos, and because it's inside thetenacity.retry(5)policy the same empty query is re-issued five times before failing. Guard the empty case (and skip retrying it). The follow-uprequests.get(url, timeout=60)also omitsraise_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 valueAnthropic model is hardcoded while OpenAI uses
config["ai"]["model"].The provider branches are inconsistent:
claude-3-7-sonnet-latestis 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 winUse environment variables for API keys instead of committed config values.
The config defines
openai_api_key,anthropic_api_key,elevenlabs_api_key, andpexels_api_keyas empty strings. While safe as a template, this pattern encourages filling them in and accidentally committing secrets. Consider havingload_configoverride 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 cfgAlso 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
📒 Files selected for processing (11)
shorts_pipeline/config.yamlshorts_pipeline/database/db.pyshorts_pipeline/database/models.pyshorts_pipeline/main.pyshorts_pipeline/modules/analytics.pyshorts_pipeline/modules/idea_generator.pyshorts_pipeline/modules/thumbnail_generator.pyshorts_pipeline/modules/uploader.pyshorts_pipeline/modules/video_assembler.pyshorts_pipeline/modules/voice_synthesizer.pyshorts_pipeline/requirements.txt
| def load_config(): | ||
| with open("config.yaml", "r", encoding="utf-8") as f: | ||
| return yaml.safe_load(f) |
There was a problem hiding this comment.
🎯 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.
| 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.
| with session_scope(sf) as session: | ||
| try: | ||
| asyncio.run(VoiceSynthesizer(cfg).synthesize_pending(session)) | ||
| except Exception: | ||
| logger.exception("Voice step failed") |
There was a problem hiding this comment.
🩺 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.
| 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] |
There was a problem hiding this comment.
🎯 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.
| 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".
| 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") |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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) |
There was a problem hiding this comment.
🗄️ 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.
| 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()) |
There was a problem hiding this comment.
🩺 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.
| 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) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Missing order_by and naive datetime inconsistency in upload_due
Two issues in this method:
-
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 indexi, so the same video could get different schedule slots between runs. Add.order_by(Video.id)for deterministic scheduling. -
Line 67 —
datetime.utcnow()returns a naive datetime, while_next_schedule(Line 36) returns a timezone-aware datetime (datetime.now(timezone.utc)). This creates a mismatch betweenscheduled_at(aware) anduploaded_at(naive) in the same table. If any downstream code compares these fields, it will raiseTypeError. Usedatetime.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.
| 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.
| 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) |
There was a problem hiding this comment.
🩺 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.
| 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) |
There was a problem hiding this comment.
🚀 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 -SRepository: 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 | |||
There was a problem hiding this comment.
🔒 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}")
PYRepository: 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
[HIGH] 1-1: pillow 10.4.0: Pillow has an OOB Write with Invalid PSD Tile Extents (Integer Overflow)
[HIGH] 1-1: pillow 10.4.0: Pillow has a PDF Parsing Trailer Infinite Loop (DoS)
[HIGH] 1-1: pillow 10.4.0: FITS GZIP decompression bomb in Pillow
[HIGH] 1-1: pillow 10.4.0: Pillow has an integer overflow when processing fonts
[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
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has CRLF injection through multipart part content type header construction
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has late size enforcement for non-file multipart fields causes memory DoS
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: HTTP/1 Pipelined Requests Queue Without Limit
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: TLS Server Hostname Override Is Ignored When Reusing HTTPS Connections
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to brute-force leak of internal static file path components
[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
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: C HTTP Parser Bypasses max_line_size for Fragmented Lines
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP's unicode processing of header values could cause parsing discrepancies
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to denial of service through large payloads
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP's HTTP Parser auto_decompress feature is vulnerable to zip bomb
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP is vulnerable to HTTP Request/Response Smuggling through incorrect parsing of chunked trailer sections
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP leaks Cookie and Proxy-Authorization headers on cross-origin redirect
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: Payload Response Resources Are Not Closed After Mid-Body Disconnect
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP accepts duplicate Host headers
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP Vulnerable to Cookie Parser Warning Storm
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: Unread Compressed Request Bodies Bypass client_max_size During Cleanup
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to DoS through chunked messages
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP Affected by Denial of Service (DoS) via Unbounded DNS Cache in TCPConnector
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP is vulnerable to cross-origin redirect with per-request cookies
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: DigestAuthMiddleware Applies Credentials to Cross-Origin Redirect Challenges
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP is Vulnerable to Deserialization of Untrusted Data
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP vulnerable to DoS when bypassing asserts
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has a Multipart Header Size Bypass
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: CRLF injection in multipart headers
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has unicode match groups in regexes for ASCII protocol elements
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP has HTTP response splitting via \r in reason phrase
[CRITICAL] 1-1: aiohttp 3.11.18: AIOHTTP affected by UNC SSRF/NTLMv2 Credential Theft/Local File Read in static resource handler on Windows
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp allows unlimited trailer headers, leading to possible uncapped memory usage
[CRITICAL] 1-1: aiohttp 3.11.18: aiohttp: Incomplete websocket frame payloads bypass memory limits
🤖 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
Summary
Problem
Solution
Submission Checklist
diff-cover) meet the gate enforced by.github/workflows/ci-lite.yml. Runpnpm test:coverageandpnpm test:rustlocally; PRs below 80% on changed lines will not merge.docs/TEST-COVERAGE-MATRIX.mdreflect this change (orN/A: behaviour-only change)## Relateddocs/RELEASE-MANUAL-SMOKE.md)Closes #NNNin the## RelatedsectionImpact
Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
Validation Run
pnpm --filter openhuman-app format:checkpnpm typecheckValidation Blocked
command:error:impact:Behavior Changes
Parity Contract
Duplicate / Superseded PR Handling
Summary by CodeRabbit