fix(studio): accept sources longer than two hours

Screening rejected every source over two hours, which blocks most long
interviews and podcasts (a 2 h 22 min Dwarkesh episode failed immediately).
The subtitle route already chunks long transcripts and scales clip counts by
hour, so drop the upper bound. Frame-by-frame visual analysis keeps its
two-hour cost cap: longer sources are routed to subtitles instead of failing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
周小舟
2026-09-30 21:14:58 +08:00
co-authored by Claude Opus 5.5
parent ec42ba368f
commit 52eaba55f7
2 changed files with 14 additions and 3 deletions
+6 -3
View File
@@ -17,11 +17,14 @@ class Recommendation(BaseModel):
suggested_goals: list[Literal['content', 'highlight', 'promo']] | None = Field(default=None, max_length=3)
VISUAL_MAX_SOURCE_SEC = 7200 # frame-by-frame vision analysis cost; longer sources use the subtitle route
def recommend(video: Path, options: ImportOptions):
info = intelligence._probe(video)
duration = info.get('duration', 0)
if duration < 1 or duration > 7200:
raise ValueError('智能制作目前支持 1 秒至 2 小时的素材')
if duration < 1:
raise ValueError('素材时长无法读取或不足 1 秒')
consent = analysis_preferences.load()
configured = intelligence.ready()
mode = 'manual'
@@ -85,6 +88,6 @@ def recommend(video: Path, options: ImportOptions):
prefs = Preferences(goal=result.goal, language=options.language,
aspect=options.aspect or result.aspect, duration=options.duration or result.duration)
suggested = list(dict.fromkeys(result.suggested_goals if result.suggested_goals is not None else (["highlight", "promo"] if mode == 'ai' and result.content_type == 'gameplay' else [result.goal])))
route = 'visual' if result.goal != 'content' and (consent.analysis_mode == 'visual' or (consent.analysis_mode == 'auto' and mode == 'ai' and result.goal != 'content')) else 'subtitle'
route = 'visual' if duration <= VISUAL_MAX_SOURCE_SEC and result.goal != 'content' and (consent.analysis_mode == 'visual' or (consent.analysis_mode == 'auto' and mode == 'ai' and result.goal != 'content')) else 'subtitle'
return {**({'local_evidence': local_evidence} if local_evidence else {}), 'analysis_preferences': consent.model_dump(), 'recommended_analysis': route, **({'diagnostics': diagnostics} if diagnostics else {}), 'mode': mode, 'source_duration': duration, **result.model_dump(), 'suggested_goals': suggested, 'preferences': prefs.model_dump(),
'overrides': options.model_dump(exclude_none=True)}
+8
View File
@@ -212,6 +212,14 @@ def test_manual_correction_does_not_call_classifier(source,monkeypatch,goal):
plan=planning.recommend(source,ImportOptions(goal=goal))
assert plan['mode']=='manual' and plan['preferences']['goal']==goal
@pytest.mark.parametrize('hours, route', [(1, 'visual'), (4, 'subtitle')])
def test_long_sources_are_accepted_and_skip_costly_visual_analysis(source, monkeypatch, hours, route):
real_probe = intelligence._probe
monkeypatch.setattr(intelligence, '_probe', lambda video: {**real_probe(video), 'duration': hours * 3600.0})
plan = planning.recommend(source, ImportOptions(goal='highlight'))
assert plan['source_duration'] == hours * 3600.0
assert plan['recommended_analysis'] == route
def test_missing_vision_is_explicit_fallback_not_fake_ai(source,monkeypatch):
monkeypatch.setattr(intelligence,'ready',lambda:False)
plan=planning.recommend(source,ImportOptions())