Skip to content

feat(podcasts): short default brief length with seconds and unit picker#1501

Merged
CREDO23 merged 16 commits into
MODSetter:mainfrom
CREDO23:feat/podcast-brief-duration-seconds
Jun 16, 2026
Merged

feat(podcasts): short default brief length with seconds and unit picker#1501
CREDO23 merged 16 commits into
MODSetter:mainfrom
CREDO23:feat/podcast-brief-duration-seconds

Conversation

@CREDO23

@CREDO23 CREDO23 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Store podcast brief target length as min_seconds / max_seconds (defaults 20s–30s) with backward-compatible loading of legacy min_minutes specs in JSONB.
  • Add shared duration bounds (15s minimum, 24h maximum unchanged).
  • Brief review UI: seconds/minutes/hours unit dropdown with clamping to global limits.

Test plan

  • uv run pytest tests/unit/podcasts/test_spec.py
  • uv run pytest tests/unit/podcasts/
  • Create podcast → brief shows 20/30s defaults
  • Switch unit to Hours → set 1h min/max → approve → episode targets ~1 hour
  • Legacy brief with min_minutes still loads in UI

High-level PR Summary

This PR migrates podcast brief duration specifications from minute-based to second-based storage, enabling finer granularity with new defaults of 20–30 seconds instead of 10–20 minutes. It adds a duration unit picker (seconds/minutes/hours) to the brief review UI with proper clamping between 15 seconds and 24 hours, while maintaining backward compatibility with existing podcast briefs stored using the legacy min_minutes and max_minutes fields through automatic migration logic on both backend and frontend.

⏱️ Estimated Review Time: 15-30 minutes

💡 Review Order Suggestion
Order File Path
1 surfsense_backend/app/podcasts/duration_limits.py
2 surfsense_backend/app/podcasts/schemas/spec.py
3 surfsense_web/contracts/types/podcast.types.ts
4 surfsense_backend/app/podcasts/api/schemas.py
5 surfsense_backend/app/podcasts/generation/brief/config.py
6 surfsense_backend/app/podcasts/generation/brief/propose.py
7 surfsense_backend/app/podcasts/generation/brief/nodes.py
8 surfsense_backend/app/podcasts/generation/transcript/nodes.py
9 surfsense_backend/app/podcasts/api/routes.py
10 surfsense_web/components/tool-ui/podcast/brief-review.tsx
11 surfsense_backend/tests/unit/podcasts/test_spec.py
12 surfsense_backend/tests/unit/podcasts/conftest.py
13 surfsense_backend/tests/integration/podcasts/conftest.py
14 surfsense_backend/tests/unit/podcasts/test_renderer.py

Need help? Join our Discord

Summary by CodeRabbit

  • New Features
    • The podcast brief now lets you set target length in seconds, minutes, or hours, then stores it as seconds.
  • Improvements / Bug Fixes
    • Podcast duration validation and brief/spec generation are now seconds-based, including min/max bounds and ordering.
    • Legacy minute-based duration inputs are automatically converted to seconds to maintain compatibility.
    • Outline sizing now scales using the seconds-based target duration.
  • Tests
    • Updated podcast spec and renderer/unit/integration tests to use seconds-based duration targets.

@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

@CREDO23 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77eaf05e-57a6-46dd-98fe-734b6ee0ef25

📥 Commits

Reviewing files that changed from the base of the PR and between 7a415b6 and 7584312.

📒 Files selected for processing (1)
  • surfsense_backend/app/podcasts/schemas/spec.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • surfsense_backend/app/podcasts/schemas/spec.py

📝 Walkthrough

Walkthrough

All podcast duration fields are migrated from minutes to seconds across the full stack. A new shared duration_limits.py module provides bounds constants. DurationTarget gains seconds fields with a legacy JSONB coercion validator. The brief generation pipeline, API schema, and frontend BriefReview component are updated accordingly, with the UI adding a unit selector for seconds/minutes/hours. Additionally, quota error test mocks are simplified to use balance_micros.

Changes

Minutes-to-Seconds Duration Migration

Layer / File(s) Summary
Shared duration limit constants
surfsense_backend/app/podcasts/duration_limits.py, surfsense_web/contracts/types/podcast.types.ts
New backend module and frontend constants export MAX_DURATION_SECONDS, MIN_DURATION_SECONDS, DEFAULT_MIN_SECONDS, DEFAULT_MAX_SECONDS as shared sources of truth.
DurationTarget schema: seconds fields and legacy migration
surfsense_backend/app/podcasts/schemas/spec.py, surfsense_web/contracts/types/podcast.types.ts
Backend DurationTarget replaces min_minutes/max_minutes with min_seconds/max_seconds, adds a mode="before" validator coercing legacy JSONB, introduces midpoint_seconds, and derives midpoint_minutes from it. Frontend zod schema preprocesses legacy minute inputs and validates the new bounds.
Brief generation pipeline
surfsense_backend/app/podcasts/generation/brief/config.py, .../brief/propose.py, .../brief/nodes.py, .../transcript/nodes.py
BriefConfig, propose_brief signature, configurable keys, spec construction in brief nodes, and target_words calculation in transcript nodes all switch to seconds-based fields.
API request schema and route
surfsense_backend/app/podcasts/api/schemas.py, surfsense_backend/app/podcasts/api/routes.py
CreatePodcastRequest replaces min_minutes/max_minutes Pydantic fields with min_seconds/max_seconds bounded by the shared constants; the create_podcast route passes the updated field names to propose_brief.
Frontend brief-review unit-aware duration editor
surfsense_web/components/tool-ui/podcast/brief-review.tsx
BriefReview gains durationUnit state, replaces fixed minutes inputs with a unit-selectable Target length editor, and adds helper functions for seconds↔unit conversion, step sizing, bounds, and clamping. Approve button validation is updated to compare seconds fields.
Test fixtures and spec/renderer unit tests
surfsense_backend/tests/integration/podcasts/conftest.py, surfsense_backend/tests/unit/podcasts/conftest.py, surfsense_backend/tests/unit/podcasts/test_renderer.py, surfsense_backend/tests/unit/podcasts/test_spec.py
All test fixtures and test cases construct DurationTarget with min_seconds/max_seconds; spec tests cover inverted-range validation, midpoint_seconds, midpoint_minutes, and legacy minute loading.

Quota Error Payload Update

Layer / File(s) Summary
Quota error mock updates
surfsense_backend/tests/integration/podcasts/test_draft_task.py, surfsense_backend/tests/unit/services/test_quota_checked_vision_llm.py, surfsense_backend/tests/unit/tasks/test_video_presentation_billing.py
Mocked QuotaInsufficientError payloads are updated to use balance_micros=5_000_000 instead of separate used_micros and limit_micros fields.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Tick-tock, the minutes must go,
Seconds now run the whole podcast show!
From brief to transcript, the clocks all align,
Legacy minutes? A validator's fine.
Hop along, time flows smoother than ever—
Seconds and rabbits: swift, light, and clever! ⏱️✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the primary changes: migration from minute-based to second-based duration storage and the addition of a UI unit picker for flexible duration specification.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@surfsense_backend/app/podcasts/api/schemas.py`:
- Around line 37-46: The CreatePodcastRequest schema validates min_seconds and
max_seconds independently, allowing inverted ranges like min_seconds=100,
max_seconds=50 to pass validation at the API boundary. Add a Pydantic validator
(root_validator or field_validator depending on your Pydantic version) to the
CreatePodcastRequest class that enforces the constraint min_seconds <=
max_seconds, ensuring invalid duration ranges are rejected with a 422 validation
error at the API boundary instead of causing a 500 error later when
DurationTarget is instantiated.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1916dad0-43a0-40f1-b4e1-5284e4534756

📥 Commits

Reviewing files that changed from the base of the PR and between 683a827 and fd96c93.

📒 Files selected for processing (14)
  • surfsense_backend/app/podcasts/api/routes.py
  • surfsense_backend/app/podcasts/api/schemas.py
  • surfsense_backend/app/podcasts/duration_limits.py
  • surfsense_backend/app/podcasts/generation/brief/config.py
  • surfsense_backend/app/podcasts/generation/brief/nodes.py
  • surfsense_backend/app/podcasts/generation/brief/propose.py
  • surfsense_backend/app/podcasts/generation/transcript/nodes.py
  • surfsense_backend/app/podcasts/schemas/spec.py
  • surfsense_backend/tests/integration/podcasts/conftest.py
  • surfsense_backend/tests/unit/podcasts/conftest.py
  • surfsense_backend/tests/unit/podcasts/test_renderer.py
  • surfsense_backend/tests/unit/podcasts/test_spec.py
  • surfsense_web/components/tool-ui/podcast/brief-review.tsx
  • surfsense_web/contracts/types/podcast.types.ts

Comment on lines +37 to +46
min_seconds: int = Field(
default=DEFAULT_MIN_SECONDS,
ge=MIN_DURATION_SECONDS,
le=MAX_DURATION_SECONDS,
)
max_seconds: int = Field(
default=DEFAULT_MAX_SECONDS,
ge=MIN_DURATION_SECONDS,
le=MAX_DURATION_SECONDS,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Check CreatePodcastRequest duration validation:"
rg -n -C2 'class CreatePodcastRequest|min_seconds|max_seconds|model_validator|_check_duration_order' surfsense_backend/app/podcasts/api/schemas.py

echo
echo "Check downstream DurationTarget construction that currently relies on ordered bounds:"
rg -n -C2 'DurationTarget\(|min_seconds=brief\.min_seconds|max_seconds=brief\.max_seconds' surfsense_backend/app/podcasts/generation/brief/nodes.py

Repository: MODSetter/SurfSense

Length of output: 922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check complete CreatePodcastRequest class ==="
rg -A 30 'class CreatePodcastRequest' surfsense_backend/app/podcasts/api/schemas.py

echo
echo "=== Find and examine DurationTarget ==="
fd -e py | xargs rg -l 'class DurationTarget' 2>/dev/null | head -5

echo
echo "=== Check DurationTarget implementation ==="
rg -B 2 -A 15 'class DurationTarget' --type py

Repository: MODSetter/SurfSense

Length of output: 2683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Read full DurationTarget class ==="
rg -A 50 'class DurationTarget' surfsense_backend/app/podcasts/schemas/spec.py

Repository: MODSetter/SurfSense

Length of output: 2097


Enforce duration ordering in CreatePodcastRequest.

min_seconds and max_seconds are validated independently, so inverted ranges pass request validation and fail later when DurationTarget is instantiated in surfsense_backend/app/podcasts/generation/brief/nodes.py (line 82) with a 500 error. Reject this at the API boundary with a 422 validation error instead.

Suggested fix
-from pydantic import BaseModel, ConfigDict, Field
+from pydantic import BaseModel, ConfigDict, Field, model_validator
@@
 class CreatePodcastRequest(BaseModel):
@@
     max_seconds: int = Field(
         default=DEFAULT_MAX_SECONDS,
         ge=MIN_DURATION_SECONDS,
         le=MAX_DURATION_SECONDS,
     )
     focus: str | None = Field(default=None, max_length=2000)
+
+    `@model_validator`(mode="after")
+    def _check_duration_order(self) -> "CreatePodcastRequest":
+        if self.max_seconds < self.min_seconds:
+            raise ValueError("max_seconds must be >= min_seconds")
+        return self
🤖 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 `@surfsense_backend/app/podcasts/api/schemas.py` around lines 37 - 46, The
CreatePodcastRequest schema validates min_seconds and max_seconds independently,
allowing inverted ranges like min_seconds=100, max_seconds=50 to pass validation
at the API boundary. Add a Pydantic validator (root_validator or field_validator
depending on your Pydantic version) to the CreatePodcastRequest class that
enforces the constraint min_seconds <= max_seconds, ensuring invalid duration
ranges are rejected with a 422 validation error at the API boundary instead of
causing a 500 error later when DurationTarget is instantiated.

CREDO23 added 2 commits June 16, 2026 23:56
Billable calls now raise quota errors with balance_micros instead of
used_micros/limit_micros; update mocks so CI passes on main.
Remove duplicate typing import and format legacy minute coercion guard.
@CREDO23 CREDO23 merged commit 284df84 into MODSetter:main Jun 16, 2026
6 of 10 checks passed
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.

1 participant