Skip to content

feat: video visual extraction (FFmpeg + Vision API)#751

Open
qiffang wants to merge 16 commits into
mainfrom
dat9-dev1/video-visual-extract
Open

feat: video visual extraction (FFmpeg + Vision API)#751
qiffang wants to merge 16 commits into
mainfrom
dat9-dev1/video-visual-extract

Conversation

@qiffang

@qiffang qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds video_extract_visual semantic task type for extracting searchable text from video files
  • FFmpeg samples frames at configurable intervals, then a Vision model (OpenAI-compatible) describes the visual content
  • Supports mp4, mov, avi, webm, mkv content types
  • Full pipeline: enqueue on upload → semantic worker claims → FFmpeg frame extraction → Vision API → content_text writeback → auto-embed

Implementation

  • pkg/semantic/task.go: TaskTypeVideoExtractVisual + VideoExtractTaskPayload
  • pkg/backend/video_extract.go: VideoTextExtractor interface, ProcessVideoExtractTask handler, MIME allowlist
  • pkg/backend/video_extract_openai.go: OpenAIVideoTextExtractor with FFmpeg + multi-image Vision API
  • pkg/backend/options.go: AsyncVideoExtractOptions config
  • pkg/backend/semantic_tasks.go: enqueue helpers + routing
  • pkg/backend/llm_usage.go: cost tracking (reuses vision token rate)
  • pkg/server/tenant_worker_semantic.go: dispatch + spec conversion
  • pkg/server/tenant_worker.go: task type registration
  • cmd/drive9-server/main.go: DRIVE9_VIDEO_EXTRACT_* env var wiring

Configuration

DRIVE9_VIDEO_EXTRACT_ENABLED=true
DRIVE9_VIDEO_EXTRACT_API_BASE, DRIVE9_VIDEO_EXTRACT_API_KEY, DRIVE9_VIDEO_EXTRACT_MODEL
DRIVE9_VIDEO_EXTRACT_FRAME_INTERVAL=5, DRIVE9_VIDEO_EXTRACT_MAX_FRAMES=10
DRIVE9_VIDEO_EXTRACT_MAX_BYTES=200MB, DRIVE9_FFMPEG_PATH=ffmpeg

Test plan

  • go build ./... passes
  • go vet ./... passes
  • Unit tests for MIME detection, enqueue logic, handler outcomes
  • CI integration tests (needs Docker for MySQL testcontainers)

Summary by CodeRabbit

  • New Features
    • Added async, durable video visual extraction for supported uploads; extracted searchable text is persisted and can enqueue semantic embedding tasks.
    • Introduced an OpenAI-compatible video analyzer with configurable prompt, FFmpeg frame sampling, and extraction/byte limits.
    • Added env-driven enablement with tenant allowlisting and a separate per-tenant video quota (video takes precedence over audio for the same revision).
  • Tests
    • Expanded backend and end-to-end coverage for routing, MIME/allowlist handling, quota behavior, and extraction outcomes.
  • Chores
    • Updated the container image to include FFmpeg.

Implements video_extract_visual semantic task type that extracts
searchable text from video files by sampling frames with FFmpeg and
describing them via a Vision model (OpenAI-compatible).

- New TaskTypeVideoExtractVisual in pkg/semantic/task.go
- VideoTextExtractor interface + ProcessVideoExtractTask handler
- OpenAIVideoTextExtractor: FFmpeg frame sampling + multi-image Vision API
- Content-type allowlist: mp4, mov, avi, webm, mkv
- Full backend wiring: options, enqueue, LLM cost tracking
- Semantic worker dispatch + tenant worker task type registration
- Env var configuration: DRIVE9_VIDEO_EXTRACT_*
- Unit tests for handler, MIME detection, enqueue logic

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds configurable asynchronous video-to-text extraction using durable semantic tasks, FFmpeg frame sampling, OpenAI-compatible vision APIs, transactional content_text updates, usage accounting, worker routing, quotas, and end-to-end tests.

Changes

Video visual extraction

Layer / File(s) Summary
Runtime configuration
cmd/drive9-server/main.go, pkg/backend/options.go, pkg/backend/dat9.go, Dockerfile
Environment variables configure extraction credentials, limits, timeout, FFmpeg settings, tenant targeting, quotas, and backend activation; the image installs FFmpeg.
Extractor and processing pipeline
pkg/backend/video_extract.go, pkg/backend/video_extract_openai.go, pkg/backend/llm_usage.go
Video bytes are size-limited, converted to sampled frames, submitted to an OpenAI-compatible vision endpoint, usage is recorded, and extracted text is written to confirmed revisions.
Durable task scheduling and workers
pkg/semantic/task.go, pkg/backend/semantic_tasks.go, pkg/server/tenant_worker.go, pkg/server/tenant_worker_semantic.go, pkg/backend/quota.go
Adds video extraction tasks, MIME and tenant gating, separate video quotas, audio suppression for videos, and worker dispatch with retry and acknowledgement handling.
Validation
pkg/backend/video_extract_test.go, pkg/server/upload_test.go, pkg/server/tenant_worker_helpers_test.go, cmd/drive9-server/video_extract_env_test.go
Tests extraction outcomes, MIME detection, tenant allowlists, quotas, video/audio coordination, environment configuration, worker fixtures, and end-to-end upload processing.

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

Sequence Diagram(s)

sequenceDiagram
  participant Upload
  participant SemanticTasks
  participant TenantWorker
  participant Dat9Backend
  participant OpenAIVideoTextExtractor
  participant OpenAICompatibleAPI
  Upload->>SemanticTasks: Confirm video revision
  SemanticTasks->>SemanticTasks: Enqueue video_extract_visual
  TenantWorker->>Dat9Backend: ProcessVideoExtractTask
  Dat9Backend->>OpenAIVideoTextExtractor: Extract video text
  OpenAIVideoTextExtractor->>OpenAICompatibleAPI: Send sampled frames
  OpenAICompatibleAPI-->>OpenAIVideoTextExtractor: Return visual text and usage
  OpenAIVideoTextExtractor-->>Dat9Backend: Return extracted text
  Dat9Backend-->>TenantWorker: Write content text and acknowledge task
Loading

Possibly related PRs

  • mem9-ai/drive9#224: Adds media file-count quota logic extended here with a separate video extraction quota.
  • mem9-ai/drive9#225: Provides shared LLM usage and monthly cost infrastructure extended here for video extraction.
  • mem9-ai/drive9#600: Also modifies the durable semantic-task enqueueing and worker-routing pipeline for media extraction.

Suggested reviewers: srstack, mornyx, jayson-huang

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ 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 clearly summarizes the main change: adding video visual extraction powered by FFmpeg and a Vision API.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dat9-dev1/video-visual-extract

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.

@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

R1 review from @adversary-1 on exact head acbb93fffb2fd43bae31b621bb660de9275bc3eb: REVISE.

Blocking findings:

  1. content_text writeback is last-writer-wins when both existing MP4 audio extraction and new visual extraction are enabled. enqueueExtractSemanticTasksTx() independently queues audio and video tasks for the same file revision (pkg/backend/semantic_tasks.go:57-88). Existing audio routing admits MP4/video containers (pkg/backend/audio_extract.go:181-188, pkg/backend/audio_extract.go:215-237). The audio handler writes transcript text with UpdateFileSearchTextTx(..., expectedRevision, text) (pkg/backend/audio_extract.go:371), and the new video handler writes visual text with the same revision gate (pkg/backend/video_extract.go:218). That datastore helper overwrites content_text (pkg/datastore/store.go:1075-1114). Result: whichever task finishes last erases the other signal. If audio finishes after visual, visual descriptions disappear and visual queries can miss even though video_extract_visual succeeded. This violates the requested visual retrieval path and the PR/thread claim that frame descriptions plus transcript are combined.

  2. Missing end-to-end proof for the actual visual retrieval contract. The new tests use a fake staticVideoExtractor and call ProcessVideoExtractTask directly (pkg/backend/video_extract_test.go:10-31). They do not exercise FFmpeg frame extraction, the OpenAI-compatible vision request/response parser, upload-time semantic task enqueue, worker dispatch, embedding/writeback, or a grep/search hit for a visual-only term. For this task, the regression needs to prove that a video whose visual content contains e.g. “dog/tree/chest” becomes searchable by that visual term, independent of audio transcript.

  3. Hosted CI is currently red on a PR-specific lint failure: pkg/backend/video_extract_openai.go:158:20 unchecked os.RemoveAll(tmpDir) (errcheck). This must be fixed before any merge consideration.

Local checks I ran:

  • git diff --check origin/main...HEAD passed.
  • go build ./pkg/backend ./pkg/server ./cmd/drive9-server passed.
  • go vet ./pkg/backend ./pkg/server ./cmd/drive9-server passed.
  • Targeted go test ./pkg/backend ... and go test ./pkg/server ... are blocked in this local environment by existing rootless Docker not found TestMain/testcontainers setup.

Required before re-review: deterministic merge/ownership semantics for audio+visual text, a regression covering both tasks on the same MP4 revision without losing visual terms, and an end-to-end or tightly mocked pipeline test that proves visual content is searchable.

When video visual extraction is enabled, skip audio_extract_text
enqueue for video MIME types to prevent last-writer-wins race on
content_text. Pure audio files (mp3, wav) still enqueue audio.

Also fix unchecked os.RemoveAll return in video_extract_openai.go.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

R2 review from @adversary-1 on exact head 86974e8a6ea5e1b1eaeace2e9b02f3979c29c743: REVISE.

R1 status:

  • B1 last-writer-wins overwrite: partially addressed. MP4 now chooses video_extract_visual over audio_extract_text, so it avoids the same-revision overwrite race by making visual extraction own video files. This is acceptable for the clarified visual-retrieval requirement if the PR no longer claims transcript+visual merge for videos.
  • B3 lint: likely addressed locally; git diff --check, go build ./pkg/backend ./pkg/server ./cmd/drive9-server, and go vet ./pkg/backend ./pkg/server ./cmd/drive9-server pass on R2. Hosted CI is still running as of this review.
  • B2 visual retrieval proof: still unresolved and remains blocking.

The new test TestMP4VideoExcludesAudioEnqueue only recomputes local booleans around shouldEnqueueVideoExtractTask / shouldEnqueueAudioExtractTask. It does not verify a durable semantic task row is enqueued on upload, does not run worker dispatch, does not run FFmpeg/Vision request/response handling, does not verify content_text writeback through the semantic worker, and does not prove a visual-only term is retrievable by grep/FTS/vector search.

For this task, direct ProcessVideoExtractTask tests with a fake extractor are not enough. The acceptance risk is exactly the integration path qiffang asked for: upload video -> enqueue visual task -> worker processes frames/Vision output -> writes searchable visual text -> search hits video by visual content. Please add an end-to-end or tightly mocked integration test that proves that path, including a visual-only query term that is not present in filename or audio transcript. Also cover the MP4 ownership decision at the task-row level, not just duplicated boolean logic.

Local package tests remain blocked in my environment by the existing rootless-Docker TestMain/testcontainers setup, so I cannot substitute local test execution for the missing regression.

Proves the full pipeline: HTTP PUT .mp4 → video_extract_visual task
enqueued → semantic worker dispatches → stub Vision extractor returns
visual description → content_text written back with visual-only term
("golden retriever") → task succeeds.

Also verifies no audio_extract_text task is enqueued for MP4 when
video extraction is enabled (mutual exclusion).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

R3 verdict: GREEN for exact head 2b0dfaa2fe313918634cc6928f368780f39d199f.

No blocking findings found in this round.

The R2 blocker is closed by TestAutoVideoPutWritesVisualContentTextEndToEnd: it now exercises HTTP PUT of an .mp4, durable video_extract_visual enqueue, semantic worker execution with the video extractor stub, visual-only content_text writeback (golden retriever), succeeded task status, and the MP4 ownership invariant that no competing audio_extract_text task is created for the same file revision. The writeback uses UpdateFileSearchTextTx, which updates the semantic search table, and existing grep coverage already validates searching indexed content_text.

Validation:

  • PASS: hosted ci
  • PASS: hosted drive9-server-local e2e smoke
  • PASS: git diff --check origin/main...HEAD
  • PASS: go build ./pkg/backend ./pkg/server ./cmd/drive9-server
  • PASS: go vet ./pkg/backend ./pkg/server ./cmd/drive9-server
  • BLOCKED locally by existing rootless-Docker/TestMain setup: go test ./pkg/server -run TestAutoVideoPutWritesVisualContentTextEndToEnd -count=1
  • BLOCKED locally by existing rootless-Docker/TestMain setup: go test ./pkg/backend -run 'TestMP4VideoExcludesAudioEnqueue|TestProcessVideoExtractTaskWritesContentText' -count=1

Scope boundary: this is code/test GREEN for visual video extraction into Drive9 search text. It does not assert audio+visual transcript merging; current MP4 semantics deliberately make visual extraction own video files to avoid content_text last-writer races.

@qiffang
qiffang marked this pull request as ready for review July 18, 2026 01:56

@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: 3

🧹 Nitpick comments (1)
pkg/backend/video_extract.go (1)

126-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the sentinel to ErrVideoExtractSourceTooLarge.

Update its errors.Is call sites accordingly. As per coding guidelines, “Name sentinel errors with the ErrFoo pattern.”

🤖 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 `@pkg/backend/video_extract.go` around lines 126 - 127, Rename the sentinel
error variable errVideoExtractSourceTooLarge to the exported
ErrVideoExtractSourceTooLarge, and update every errors.Is call site and other
references to use the new name while preserving the existing error value and
behavior.

Source: Coding guidelines

🤖 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 `@pkg/backend/llm_usage.go`:
- Around line 98-126: Update recordVideoExtractUsage to accept ctx
context.Context as its first parameter, then use that context for logging and
syncCentralLLMCostRecord instead of backgroundWithTrace(). Update
ProcessVideoExtractTask to pass its existing context when invoking
recordVideoExtractUsage, preserving the current usage-recording behavior.

In `@pkg/backend/video_extract_test.go`:
- Around line 40-42: Replace assertion-only t.Fatal/t.Fatalf calls with the
specified t.Error/t.Errorf variants throughout pkg/backend/video_extract_test.go
at lines 40-42, 44-46, 59-61, 62-64, 85-87, 107-109, 130-132, 161-163, 164-166,
167-169, 194-196, 206-208, 209-211, 231-233, 237-239, 244-246, and 247-249. In
pkg/server/upload_test.go, change lines 684-686, 687-689, and 690-692 to
t.Errorf, and add an immediate return after the length assertion at 684-686 to
prevent indexing an empty result; keep fatal calls only for setup failures.

In `@pkg/backend/video_extract.go`:
- Around line 258-291: The video extraction flow must enforce an end-to-end
memory budget instead of retaining the full source, extracted JPEGs, base64
data, and JSON simultaneously. Update Dat9Backend.loadVideoBytesForExtract in
pkg/backend/video_extract.go (lines 258-291) to stream or bound S3/source data
through a temporary file rather than returning the entire source in memory, and
update the frame-processing/request assembly flow in
pkg/backend/video_extract_openai.go (lines 153-238) to cap or scale frame data
before constructing the OpenAI payload.

---

Nitpick comments:
In `@pkg/backend/video_extract.go`:
- Around line 126-127: Rename the sentinel error variable
errVideoExtractSourceTooLarge to the exported ErrVideoExtractSourceTooLarge, and
update every errors.Is call site and other references to use the new name while
preserving the existing error value and behavior.
🪄 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: 81e3bd19-6cb4-471f-b790-49830e3b30b4

📥 Commits

Reviewing files that changed from the base of the PR and between bfd070e and 2b0dfaa.

📒 Files selected for processing (13)
  • cmd/drive9-server/main.go
  • pkg/backend/dat9.go
  • pkg/backend/llm_usage.go
  • pkg/backend/options.go
  • pkg/backend/semantic_tasks.go
  • pkg/backend/video_extract.go
  • pkg/backend/video_extract_openai.go
  • pkg/backend/video_extract_test.go
  • pkg/semantic/task.go
  • pkg/server/tenant_worker.go
  • pkg/server/tenant_worker_helpers_test.go
  • pkg/server/tenant_worker_semantic.go
  • pkg/server/upload_test.go

Comment thread pkg/backend/llm_usage.go
Comment on lines +98 to +126
func (b *Dat9Backend) recordVideoExtractUsage(taskID string, usage VideoExtractUsage) {
totalTokens := int64(usage.TotalTokens())
cost := b.imageTokenCostMillicents(totalTokens) // reuse vision cost rate
if cost == 0 && totalTokens == 0 {
cost = b.fallbackImageCostMillicents
}
if cost <= 0 {
return
}
if !b.UseServerQuota() {
if err := b.store.InsertLLMUsage("video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
logger.Warn(backgroundWithTrace(), "llm_usage_insert_failed",
zap.String("tenant_id", b.tenantID),
zap.String("task_type", "video_extract_visual"),
zap.String("task_id", taskID),
zap.Error(err))
metrics.RecordTenantOperation(b.tenantID, "llm_cost_budget", "usage_insert", "error", 0)
}
}
if err := b.syncCentralLLMCostRecord(backgroundWithTrace(), "video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
logger.Warn(backgroundWithTrace(), "central_quota_llm_cost_record_failed",
zap.String("tenant_id", b.tenantID),
zap.String("task_type", "video_extract_visual"),
zap.String("task_id", taskID),
zap.Error(err))
metrics.RecordTenantOperation(b.tenantID, "central_quota", "llm_cost_record", "log_error", 0)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Accept and pass context.Context to propagate tracing and cancellation.

As per coding guidelines, pass context.Context as the first parameter to all I/O and database calls. The recordVideoExtractUsage function initiates database and quota operations but does not accept a context parameter, relying instead on a disconnected backgroundWithTrace() context.

Please update the function signature to accept ctx context.Context as the first parameter and pass it to the logger and syncCentralLLMCostRecord. Note that you will also need to update its call site in ProcessVideoExtractTask to pass the context.

💡 Proposed fix
-func (b *Dat9Backend) recordVideoExtractUsage(taskID string, usage VideoExtractUsage) {
+func (b *Dat9Backend) recordVideoExtractUsage(ctx context.Context, taskID string, usage VideoExtractUsage) {
 	totalTokens := int64(usage.TotalTokens())
 	cost := b.imageTokenCostMillicents(totalTokens) // reuse vision cost rate
 	if cost == 0 && totalTokens == 0 {
 		cost = b.fallbackImageCostMillicents
 	}
 	if cost <= 0 {
 		return
 	}
 	if !b.UseServerQuota() {
 		if err := b.store.InsertLLMUsage("video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
-			logger.Warn(backgroundWithTrace(), "llm_usage_insert_failed",
+			logger.Warn(ctx, "llm_usage_insert_failed",
 				zap.String("tenant_id", b.tenantID),
 				zap.String("task_type", "video_extract_visual"),
 				zap.String("task_id", taskID),
 				zap.Error(err))
 			metrics.RecordTenantOperation(b.tenantID, "llm_cost_budget", "usage_insert", "error", 0)
 		}
 	}
-	if err := b.syncCentralLLMCostRecord(backgroundWithTrace(), "video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
+	if err := b.syncCentralLLMCostRecord(ctx, "video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
-		logger.Warn(backgroundWithTrace(), "central_quota_llm_cost_record_failed",
+		logger.Warn(ctx, "central_quota_llm_cost_record_failed",
 			zap.String("tenant_id", b.tenantID),
 			zap.String("task_type", "video_extract_visual"),
 			zap.String("task_id", taskID),
 			zap.Error(err))
 		metrics.RecordTenantOperation(b.tenantID, "central_quota", "llm_cost_record", "log_error", 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
func (b *Dat9Backend) recordVideoExtractUsage(taskID string, usage VideoExtractUsage) {
totalTokens := int64(usage.TotalTokens())
cost := b.imageTokenCostMillicents(totalTokens) // reuse vision cost rate
if cost == 0 && totalTokens == 0 {
cost = b.fallbackImageCostMillicents
}
if cost <= 0 {
return
}
if !b.UseServerQuota() {
if err := b.store.InsertLLMUsage("video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
logger.Warn(backgroundWithTrace(), "llm_usage_insert_failed",
zap.String("tenant_id", b.tenantID),
zap.String("task_type", "video_extract_visual"),
zap.String("task_id", taskID),
zap.Error(err))
metrics.RecordTenantOperation(b.tenantID, "llm_cost_budget", "usage_insert", "error", 0)
}
}
if err := b.syncCentralLLMCostRecord(backgroundWithTrace(), "video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
logger.Warn(backgroundWithTrace(), "central_quota_llm_cost_record_failed",
zap.String("tenant_id", b.tenantID),
zap.String("task_type", "video_extract_visual"),
zap.String("task_id", taskID),
zap.Error(err))
metrics.RecordTenantOperation(b.tenantID, "central_quota", "llm_cost_record", "log_error", 0)
}
}
func (b *Dat9Backend) recordVideoExtractUsage(ctx context.Context, taskID string, usage VideoExtractUsage) {
totalTokens := int64(usage.TotalTokens())
cost := b.imageTokenCostMillicents(totalTokens) // reuse vision cost rate
if cost == 0 && totalTokens == 0 {
cost = b.fallbackImageCostMillicents
}
if cost <= 0 {
return
}
if !b.UseServerQuota() {
if err := b.store.InsertLLMUsage("video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
logger.Warn(ctx, "llm_usage_insert_failed",
zap.String("tenant_id", b.tenantID),
zap.String("task_type", "video_extract_visual"),
zap.String("task_id", taskID),
zap.Error(err))
metrics.RecordTenantOperation(b.tenantID, "llm_cost_budget", "usage_insert", "error", 0)
}
}
if err := b.syncCentralLLMCostRecord(ctx, "video_extract_visual", taskID, cost, totalTokens, "tokens"); err != nil {
logger.Warn(ctx, "central_quota_llm_cost_record_failed",
zap.String("tenant_id", b.tenantID),
zap.String("task_type", "video_extract_visual"),
zap.String("task_id", taskID),
zap.Error(err))
metrics.RecordTenantOperation(b.tenantID, "central_quota", "llm_cost_record", "log_error", 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 `@pkg/backend/llm_usage.go` around lines 98 - 126, Update
recordVideoExtractUsage to accept ctx context.Context as its first parameter,
then use that context for logging and syncCentralLLMCostRecord instead of
backgroundWithTrace(). Update ProcessVideoExtractTask to pass its existing
context when invoking recordVideoExtractUsage, preserving the current
usage-recording behavior.

Source: Coding guidelines

Comment on lines +40 to +42
if result != VideoExtractResultWritten {
t.Fatalf("result=%q, want %q", result, VideoExtractResultWritten)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use t.Errorf/t.Error for assertion failures.

As per coding guidelines, tests must use t.Fatal/t.Fatalf only for setup failures and t.Errorf/t.Error for assertion failures.

  • pkg/backend/video_extract_test.go#L40-L42: change to t.Errorf.
  • pkg/backend/video_extract_test.go#L44-L46: change to t.Errorf.
  • pkg/backend/video_extract_test.go#L59-L61: change to t.Error.
  • pkg/backend/video_extract_test.go#L62-L64: change to t.Errorf.
  • pkg/backend/video_extract_test.go#L85-L87: change to t.Errorf.
  • pkg/backend/video_extract_test.go#L107-L109: change to t.Errorf.
  • pkg/backend/video_extract_test.go#L130-L132: change to t.Errorf.
  • pkg/backend/video_extract_test.go#L161-L163: change to t.Error.
  • pkg/backend/video_extract_test.go#L164-L166: change to t.Error.
  • pkg/backend/video_extract_test.go#L167-L169: change to t.Error.
  • pkg/backend/video_extract_test.go#L194-L196: change to t.Error.
  • pkg/backend/video_extract_test.go#L206-L208: change to t.Error.
  • pkg/backend/video_extract_test.go#L209-L211: change to t.Error.
  • pkg/backend/video_extract_test.go#L231-L233: change to t.Error.
  • pkg/backend/video_extract_test.go#L237-L239: change to t.Error.
  • pkg/backend/video_extract_test.go#L244-L246: change to t.Error.
  • pkg/backend/video_extract_test.go#L247-L249: change to t.Error.
  • pkg/server/upload_test.go#L684-L686: change to t.Errorf and append an immediate return to prevent a panic on the next line if the length is 0.
  • pkg/server/upload_test.go#L687-L689: change to t.Errorf.
  • pkg/server/upload_test.go#L690-L692: change to t.Errorf.
📍 Affects 2 files
  • pkg/backend/video_extract_test.go#L40-L42 (this comment)
  • pkg/backend/video_extract_test.go#L44-L46
  • pkg/backend/video_extract_test.go#L59-L61
  • pkg/backend/video_extract_test.go#L62-L64
  • pkg/backend/video_extract_test.go#L85-L87
  • pkg/backend/video_extract_test.go#L107-L109
  • pkg/backend/video_extract_test.go#L130-L132
  • pkg/backend/video_extract_test.go#L161-L163
  • pkg/backend/video_extract_test.go#L164-L166
  • pkg/backend/video_extract_test.go#L167-L169
  • pkg/backend/video_extract_test.go#L194-L196
  • pkg/backend/video_extract_test.go#L206-L208
  • pkg/backend/video_extract_test.go#L209-L211
  • pkg/backend/video_extract_test.go#L231-L233
  • pkg/backend/video_extract_test.go#L237-L239
  • pkg/backend/video_extract_test.go#L244-L246
  • pkg/backend/video_extract_test.go#L247-L249
  • pkg/server/upload_test.go#L684-L686
  • pkg/server/upload_test.go#L687-L689
  • pkg/server/upload_test.go#L690-L692
🤖 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 `@pkg/backend/video_extract_test.go` around lines 40 - 42, Replace
assertion-only t.Fatal/t.Fatalf calls with the specified t.Error/t.Errorf
variants throughout pkg/backend/video_extract_test.go at lines 40-42, 44-46,
59-61, 62-64, 85-87, 107-109, 130-132, 161-163, 164-166, 167-169, 194-196,
206-208, 209-211, 231-233, 237-239, 244-246, and 247-249. In
pkg/server/upload_test.go, change lines 684-686, 687-689, and 690-692 to
t.Errorf, and add an immediate return after the length assertion at 684-686 to
prevent indexing an empty result; keep fatal calls only for setup failures.

Source: Coding guidelines

Comment on lines +258 to +291
func (b *Dat9Backend) loadVideoBytesForExtract(ctx context.Context, f *datastore.File) ([]byte, error) {
if b.videoExtractMaxSize > 0 && f.SizeBytes > b.videoExtractMaxSize {
return nil, errVideoExtractSourceTooLarge
}
if f.StorageType == datastore.StorageDB9 {
if b.videoExtractMaxSize > 0 && int64(len(f.ContentBlob)) > b.videoExtractMaxSize {
return nil, errVideoExtractSourceTooLarge
}
return append([]byte(nil), f.ContentBlob...), nil
}
if f.StorageType != datastore.StorageS3 {
return nil, fmt.Errorf("unsupported storage type: %s", f.StorageType)
}
if b.s3 == nil {
return nil, fmt.Errorf("s3 client not configured")
}
rc, err := b.s3.GetObject(ctx, f.StorageRef)
if err != nil {
return nil, err
}
defer func() { _ = rc.Close() }()

reader := io.Reader(rc)
if b.videoExtractMaxSize > 0 {
reader = io.LimitReader(rc, b.videoExtractMaxSize+1)
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
if b.videoExtractMaxSize > 0 && int64(len(data)) > b.videoExtractMaxSize {
return nil, errVideoExtractSourceTooLarge
}
return data, nil

Copy link
Copy Markdown

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

Add an end-to-end memory budget for video extraction.

The pipeline retains the full source, extracted JPEGs, base64 strings, and the JSON payload in memory at once. Stream or bound the source into a temp file, and cap/scale frame data before assembling the OpenAI request.

📍 Affects 2 files
  • pkg/backend/video_extract.go#L258-L291 (this comment)
  • pkg/backend/video_extract_openai.go#L153-L238
🤖 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 `@pkg/backend/video_extract.go` around lines 258 - 291, The video extraction
flow must enforce an end-to-end memory budget instead of retaining the full
source, extracted JPEGs, base64 data, and JSON simultaneously. Update
Dat9Backend.loadVideoBytesForExtract in pkg/backend/video_extract.go (lines
258-291) to stream or bound S3/source data through a temporary file rather than
returning the entire source in memory, and update the frame-processing/request
assembly flow in pkg/backend/video_extract_openai.go (lines 153-238) to cap or
scale frame data before constructing the OpenAI payload.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b0dfaa2fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

isImage := b.hasAsyncImageTextSource(path, contentType)
isAudio := b.shouldEnqueueAudioExtractTask(path, contentType)
if !isImage && !isAudio {
isVideo := b.shouldEnqueueVideoExtractTask(path, contentType)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Count video files against the media LLM quota

When video extraction is enabled, this adds video_extract_visual tasks to the same quota-gated enqueue path, but the currentMediaDelta passed by all callers is still computed with isQuotaMediaContentType, which only counts image/ and audio/ content types. A tenant at its MaxMediaLLMFiles limit can therefore keep uploading videos and each one will enqueue a Vision job without increasing the media count, bypassing the intended LLM-file cap.

Useful? React with 👍 / 👎.

Comment thread pkg/backend/semantic_tasks.go Outdated
Comment on lines +86 to +87
if isVideo {
created, err := b.enqueueVideoExtractTaskTx(tx, fileID, revision, path, contentType)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disable video extraction for FTS-only tenant profiles

For TiDB tenants whose embedding profile resolves to non-auto/FTS-only, tenant.Pool.createBackend currently clears AsyncImageExtract and AsyncAudioExtract but not the new AsyncVideoExtract. With this enqueue path, those tenants will still create and process video_extract_visual tasks whenever the server template has video extraction enabled, unexpectedly running Vision work (and incurring LLM cost) in the same mode where image/audio extraction was explicitly disabled.

Useful? React with 👍 / 👎.

Comment thread pkg/backend/llm_usage.go
Comment on lines +101 to +102
if cost == 0 && totalTokens == 0 {
cost = b.fallbackImageCostMillicents

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scale fallback video cost by extracted frames

When the Vision provider omits token usage, video extraction records only one fallbackImageCostMillicents even though the request can include multiple frames (FramesExtracted, default max 10). In that provider mode the monthly LLM budget and central cost accounting can undercount each video by the number of sampled frames, allowing tenants to exceed the intended spend cap before monthlyLLMCostExceededCheck starts skipping work.

Useful? React with 👍 / 👎.

FFmpeg is needed by the video visual extraction pipeline to sample
frames from uploaded videos before sending them to the Vision API.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

GREEN at exact head 9972980.\n\nPost-R3 delta reviewed: only Dockerfile changes from prior GREEN head 2b0dfaa, adding Alpine ffmpeg alongside ca-certificates for the product-enabled video extraction container path. This is consistent with the FFmpeg dependency introduced by frame extraction.\n\nValidation:\n- git diff --name-status 2b0dfaa..origin/pr-751 => Dockerfile only\n- git diff --check 2b0dfaa..origin/pr-751 passed\n- GitHub ci success\n- GitHub drive9-server-local e2e smoke success\n- CodeRabbit success\n\nBoundary remains unchanged: this approves the default-off product pipeline plus Docker image dependency. It does not claim real Vision API/content-effect staging smoke has been performed, and it does not add tenant allowlist/count quota controls.\n\nNote: formal GitHub approval from this machine failed with "Review Can not approve your own pull request"; this comment is the review record.

Global default remains off. When DRIVE9_VIDEO_EXTRACT_ENABLED=true,
only tenants listed in DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST (comma-
separated) may enqueue video extraction tasks. Omitting the env var
allows all tenants (backwards-compatible). An empty value blocks all.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

REVISE at exact head 05f2ce1.\n\nBlocking issue: the tenant allowlist is fail-open when the env allowlist is unset/empty. In cmd/drive9-server/main.go lines 934-949, tenantAllowlist remains nil unless DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST has a non-empty value. In pkg/backend/semantic_tasks.go lines 107-113, nil means no filtering, so DRIVE9_VIDEO_EXTRACT_ENABLED=true with no allowlist enables video extraction for every tenant. That violates the stated rollout requirement: global default false, and when enabled only explicitly selected tenants should receive video visual extraction.\n\nThe new test also codifies the wrong contract: pkg/backend/video_extract_test.go lines 243-255 asserts nil allowlist allows all tenants. That is incompatible with the requested shared-production safety gate.\n\nExpected gate for this PR/follow-up: with video extraction globally enabled but no tenant allowlist configured, either startup must fail closed or no tenants may enqueue video extraction. Non-allowlisted tenants must upload/list/stat/download normally, with no video_extract_visual task and no FFmpeg/Vision call. Allowlisted tenants should retain the happy path. Also ensure logs distinguish missing/empty allowlist from actual allowlist size, because tenant_allowlist_size=0 currently could mean either all tenants allowed or none allowed depending on nil vs empty map semantics.\n\nValidation performed: git diff --check 9972980..origin/pr-751 passed; GitHub ci/e2e are still in progress, but this is a logic/spec blocker independent of CI.

When DRIVE9_VIDEO_EXTRACT_ENABLED=true but DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST
is unset, default to empty map so no tenant can enqueue. Operators must
explicitly list tenant IDs. Addresses adversary-1 review: nil map no longer
means "all tenants allowed".

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Adversarial review: GREEN for head 1da428af68e49b91d2b237b31d6048e52d90d6d9.

The previous tenant-allowlist blocker is closed. The updated code now fails closed for nil/empty tenant allowlists, only enqueues video_extract_visual for explicitly allowlisted tenants, and keeps non-allowlisted video uploads on the normal upload path with no video visual extraction task.

Validation:

  • GitHub ci: SUCCESS
  • GitHub drive9-server-local e2e smoke: SUCCESS
  • CodeRabbit: SUCCESS
  • Local git diff --check 05f2ce1..origin/pr-751: passed
  • Local compile: go test -c ./pkg/backend ./pkg/server ./cmd/drive9-server: passed
  • Local vet: go vet ./pkg/backend ./pkg/server ./cmd/drive9-server: passed

Local focused runtime go test ./pkg/backend -run 'TestShouldEnqueueVideoExtractTask|TestVideoExtractTenantAllowlist|TestMP4VideoExcludesAudioEnqueue|TestProcessVideoExtractTask' -count=1 is still blocked by the known local rootless Docker not found TestMain/testcontainers dependency, so I relied on hosted CI/e2e for runtime coverage.

Review boundary remains explicit: this GREEN covers the default-off, tenant-allowlisted Drive9 product pipeline and Docker FFmpeg dependency. It still does not prove a real Bailian/DashScope Vision API smoke with a real MP4, real model response, final bilingual content_text, search hit, and task status evidence.

Replace DRIVE9_VIDEO_EXTRACT_ENABLED + DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST
with just DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST:
- empty/unset = off (no Vision/FFmpeg init)
- "*" = all tenants
- "tenant-a,tenant-b" = only listed tenants

Validation: "*" must be sole token (no mixing with IDs), glob patterns
like "tenant-*" are rejected. Parse errors fail startup.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Adversarial re-review for head 4d5c6ba3c359bf0e3a539db264aca7b9058b6520: REVISE.

Blocker: the new single-config parser is not covered by regression tests. The rollout safety contract now lives in cmd/drive9-server/main.go:908-941: unset/empty means off, * means all tenants, tenant IDs mean scoped enablement, tenant-* is rejected, and *,tenant-a is rejected. But the only new test coverage I see is pkg/backend/video_extract_test.go:273-284, which manually sets AllTenants: true after parsing. That proves the backend flag works, but it does not prove the env parser rejects dangerous wildcard inputs or preserves the no-init/no-key path when the allowlist is empty.

This is exactly the code path that prevents another fail-open rollout bug, so it needs tests in cmd/drive9-server or an extracted pure parser test. Minimum matrix:

  • unset and empty DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST => disabled, no API base/key/model required
  • tenant-a => enabled, AllTenants=false, allowlist contains only tenant-a
  • tenant-a,tenant-b with whitespace => both exact tenant IDs only
  • * => enabled, AllTenants=true, no tenant IDs required
  • *,tenant-a => startup/config error
  • tenant-* or any token containing * other than the exact * token => startup/config error
  • define and test empty-token behavior (",", tenant-a,,tenant-b) so it cannot silently drift

Validation I ran:

  • git diff --check 1da428af68e49b91d2b237b31d6048e52d90d6d9..HEAD: passed
  • go test -c ./pkg/backend ./pkg/server ./cmd/drive9-server: passed
  • go vet ./pkg/backend ./pkg/server ./cmd/drive9-server: passed
  • go test ./cmd/drive9-server -run 'TestBuildBackendOptionsFromEnvAudio|TestBuildAudioExtractOptionsFromEnv' -count=1: passed
  • focused backend runtime test still blocked locally by known rootless Docker not found TestMain/testcontainers dependency
  • GitHub ci and drive9-server-local e2e smoke were still in progress when this review was posted; CodeRabbit was success

The implementation direction is acceptable. The missing parser tests are the blocker.

Extract allowlist parser from main.go into backend package as a pure
function. Add table-driven tests covering: empty, single/multi tenant,
whitespace, wildcard "*", trailing comma, mixed wildcard+tenant (error),
glob patterns like "tenant-*" (error).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Adversarial re-review for head ad4df5b7897453844461c396f833b66768c76dbe: REVISE.

The previous parser-test blocker is mostly addressed, but one required empty-token edge is still undefined and currently behaves inconsistently.

ParseVideoExtractTenantAllowlist skips empty tokens at pkg/backend/options.go:195-199 and then returns the map even when no real tenant token was parsed at pkg/backend/options.go:214. Because buildVideoExtractOptionsFromEnv only short-circuits before parsing when the raw env string is empty (cmd/drive9-server/main.go:912-915), a value like DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST=, or , , is not treated as off and is not rejected. With API base/key/model present it initializes the video runtime with Enabled=true, AllTenants=false, and an empty allowlist; without those envs it fails startup. That is neither the documented empty/unset = off behavior nor an explicit invalid-config failure.

This exact class was in the prior requested matrix: define and test empty-token behavior for "," and tenant-a,,tenant-b. The new tests cover trailing comma (tenant-a,) but do not cover comma-only or interior empty-token behavior (pkg/backend/video_extract_test.go:296-306).

Required fix: make all-empty token lists either explicitly off or explicit startup/config errors; I recommend config error for , / , , because the operator supplied a malformed non-empty env. Also add tests for , and tenant-a,,tenant-b so the accepted empty-token policy is pinned. If tenant-a,,tenant-b is intentionally accepted, test it; if not, reject it.

Validation I ran:

  • git diff --check 4d5c6ba3c359bf0e3a539db264aca7b9058b6520..HEAD: passed
  • go test -c ./pkg/backend ./pkg/server ./cmd/drive9-server: passed
  • go vet ./pkg/backend ./pkg/server ./cmd/drive9-server: passed
  • go test ./cmd/drive9-server -run 'TestBuildBackendOptionsFromEnvAudio|TestBuildAudioExtractOptionsFromEnv' -count=1: passed
  • go test ./pkg/backend -run TestParseVideoExtractTenantAllowlist -count=1: locally blocked by known rootless Docker not found TestMain/testcontainers dependency
  • GitHub ci, drive9-server-local e2e smoke, and CodeRabbit were still in progress/pending when this review was posted

This is a small fix, but it is still in the rollout-control parser. I would not merge until this ambiguity is removed.

All-empty tokens (e.g. "," or ", ,") now return off instead of
initializing video runtime with an empty allowlist. Interior empty
tokens ("a,,b") are silently skipped, keeping "a" and "b". Added
three new test cases: only-commas, commas-and-spaces, interior-empty.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

REVISE at 253e3eb1ea33b29c65edb334d6ce0b78503d3569.

Blocking issue remains: comma-only allowlist is documented/tested as off in pkg/backend.ParseVideoExtractTenantAllowlist, but the startup env path still treats any non-empty raw env as enabled before checking the parsed result.

Evidence:

  • pkg/backend/options.go now returns (false, nil, nil) for "," / ", ,".
  • cmd/drive9-server/main.go:912-927 only short-circuits on raw empty string. For DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST=,, parsing returns off, but startup continues and requires DRIVE9_VIDEO_EXTRACT_API_BASE, DRIVE9_VIDEO_EXTRACT_API_KEY, and DRIVE9_VIDEO_EXTRACT_MODEL, then would initialize the video extractor if they are present.

This violates the chosen single-var contract: comma-only values are supposed to behave like off / no Vision runtime initialization. The parser unit tests are not enough because they do not cover the env-to-runtime builder.

Local proof:

DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST=, go test ./cmd/drive9-server -run TestBuildBackendOptionsFromEnvAudioDisabled -count=1

Fails with:

buildBackendOptionsFromEnv: DRIVE9_VIDEO_EXTRACT_API_BASE, DRIVE9_VIDEO_EXTRACT_API_KEY and DRIVE9_VIDEO_EXTRACT_MODEL must be set when DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST is non-empty

Required fix:

  • After parsing, if allTenants == false && tenantAllowlist == nil, return disabled options before reading/validating API env or initializing FFmpeg/Vision runtime.
  • Add a cmd/drive9-server env/startup test for DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST=, and , , proving no API/model required and AsyncVideoExtract is not wired.

git diff --check ad4df5b7897453844461c396f833b66768c76dbe..HEAD passes. I did not rerun the full suite because this startup-contract blocker is still reproducible.

Move the early-return check after ParseVideoExtractTenantAllowlist so
that all-comma inputs like "," or ", ," are treated as off and do not
require API key/model env vars.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

REVISE at 3d4dcc796df7630afe30d8e6f71661faed2fc8b1.

The runtime bug appears fixed, but the regression test is still missing.

What changed correctly:

  • buildVideoExtractOptionsFromEnv now parses DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST first.
  • If parser result is allTenants=false and tenantAllowlist=nil, it returns disabled video options before reading API envs or initializing Vision/FFmpeg.

Manual proof now passes:

DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST=, go test ./cmd/drive9-server -run TestBuildBackendOptionsFromEnvAudioDisabled -count=1

But this is not a committed regression test. cmd/drive9-server/main_test.go still has no explicit video env/startup test for comma-only values. The previous blocker was specifically an integration mismatch between ParseVideoExtractTenantAllowlist and buildVideoExtractOptionsFromEnv; parser-only tests did not catch it, and they still would not catch a future startup-path regression.

Required fix before GREEN:

  • Add committed cmd/drive9-server env/startup tests for DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST=, and DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST=, , proving no API base/key/model is required and AsyncVideoExtract is not wired.
  • Prefer also adding a scoped positive/missing-config test for a real tenant token to prove non-empty real allowlists still require API config.

Validation run:

  • git diff --check 253e3eb1ea33b29c65edb334d6ce0b78503d3569..HEAD passed.
  • Manual env-path focused test above passed.

Tests buildVideoExtractOptionsFromEnv directly with t.Setenv:
- off cases: unset, only-commas, commas-and-spaces, whitespace
- invalid: glob prefix "tenant-*", mixed wildcard "*,tenant-a"
- missing API envs with tenant allowlist set
- missing API envs with wildcard "*"

No Docker/testcontainers dependency — runs locally.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

GREEN at 5ee3ce90b78099abcfeb136620ed1b34a7b5e489.

The previous single-config allowlist blockers are fixed:

  • DRIVE9_VIDEO_EXTRACT_TENANT_ALLOWLIST is now the single control.
  • unset / whitespace / comma-only values are off and do not require API config or initialize Vision/FFmpeg runtime.
  • * is explicit global enable.
  • exact tenant allowlists remain scoped.
  • mixed wildcard (*,tenant-a) and glob/prefix wildcard (tenant-*) fail startup.
  • committed cmd/drive9-server tests now cover the env/startup integration path, not only the parser, including , and , , off cases.

Validation:

  • Local git diff --check 3d4dcc796df7630afe30d8e6f71661faed2fc8b1..HEAD passed.
  • Local focused go test ./cmd/drive9-server -run 'TestBuildVideoExtractOptionsFromEnv|TestBuildBackendOptionsFromEnvAudioDisabled|TestBuildAudioExtractOptionsFromEnv|TestBuildBackendOptionsFromEnvAudio' -count=1 passed.
  • Local go test -c ./pkg/backend ./pkg/server ./cmd/drive9-server passed.
  • Local go vet ./pkg/backend ./pkg/server ./cmd/drive9-server passed.
  • Hosted ci passed.
  • Hosted drive9-server-local e2e smoke passed.
  • CodeRabbit passed.

Boundary unchanged: this GREEN covers the default-off / allowlisted video visual extraction product pipeline and regression coverage. It still does not prove real Bailian/DashScope Vision quality/effect with a real MP4, real model response, final bilingual content_text, search hit, and disabled/over-limit no-Vision-call evidence.

Note: formal GitHub approve is unavailable from this local identity because GitHub reports it as the PR author.

Add MaxVideoLLMFiles quota independent from the existing media LLM
quota. Counts distinct resource_ids with video_extract_visual tasks.
Default 50 per tenant, configurable via DRIVE9_VIDEO_EXTRACT_MAX_FILES.
When exceeded, video files still upload normally but no extraction
tasks are enqueued.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

REVISE at exact head fcd84038f97d1b3c7ce01f6d3ca1fd770a5e5959.

Blocking issues:

  1. Off-by-one: default 50 allows the 51st new video extraction task. enqueueExtractSemanticTasksTx() calls videoLLMQuotaExceededTx() before inserting the new video_extract_visual task (pkg/backend/semantic_tasks.go:86-90), while videoLLMQuotaExceededTx() only returns true when count > b.maxVideoLLMFiles (pkg/backend/quota.go:384-389). With 50 existing distinct video task resource IDs and limit 50, the check returns false and enqueues the 51st. Fix with a projected count for the current resource, or equivalent >= handling that still permits re-enqueue/overwrite for an already counted resource. Add tests for N=limit, N=limit+1, and existing-resource overwrite/revision behavior.

  2. The video quota is not independent from the existing image/audio media quota. enqueueExtractSemanticTasksTx() still runs mediaLLMQuotaExceededCheckTx() before the video-specific check (pkg/backend/semantic_tasks.go:67-70). Since isQuotaMediaContentType() counts only image/ and audio/ (pkg/backend/quota_mutation.go:74-75), a tenant with image/audio usage over MaxMediaLLMFiles can block video extraction even when video count is under 50. That violates the option-2 boundary we just agreed on: video extraction should have its own quota and not be governed by image/audio quota. Split the checks so video-only extraction uses the video quota; keep image/audio behavior unchanged.

  3. No regression tests were added for the new quota behavior or env knob. git diff --name-only 5ee3ce9..fcd8403 | rg '_test\.go$' returns nothing. This needs committed tests covering at least: default 50, DRIVE9_VIDEO_EXTRACT_MAX_FILES, 51st video skip, no video_extract_visual enqueue/no FFmpeg/no Vision after quota, image/audio quota unchanged, and non-allowlisted tenant unchanged. Also define the intended behavior for 0/negative/invalid values instead of relying on the current fallback behavior implicitly.

Local validation I ran: git diff --check 5ee3ce9..origin/pr-751, focused cmd/drive9-server env tests, compile for ./pkg/backend ./pkg/server ./cmd/drive9-server, and go vet ./pkg/backend ./pkg/server ./cmd/drive9-server passed. Hosted ci and drive9-server-local e2e smoke were still in progress at review time; CodeRabbit was green. The correctness blockers above are independent of CI.

- Change count > max to count >= max (check is pre-enqueue, so >= is
  correct boundary for the Nth file)
- Video uses its own quota, not the general media LLM quota. Image and
  audio still share the media quota independently.
- Add tests for default (50) and custom MaxVideoLLMFiles values.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

REVISE at exact head 46d023780eae081a0c35bbef3ab717f629957e5b.

Good movement: the shared media quota is no longer applied to pure video extraction (pkg/backend/semantic_tasks.go:69-97), so the image/audio quota no longer directly gates the video enqueue branch.

Blocking issues:

  1. Existing counted videos are blocked at quota, so updated videos can keep stale visual text. videoLLMQuotaExceededTx() now returns true when COUNT(DISTINCT resource_id) >= max (pkg/backend/quota.go:384-389) and the check still happens before enqueue (pkg/backend/semantic_tasks.go:87-93). If the tenant already has 50 distinct video resource IDs and rewrites one of those same files at a new revision, the distinct video count remains 50, but the code blocks the new video_extract_visual task. That violates a “50 videos per tenant” quota; it behaves like a lifetime no-more-revisions gate. Fix by projecting whether the current fileID is already counted, e.g. block only when count >= max and current resource_id has no existing video task, or define a different lifetime-extraction contract explicitly. Add regression for existing-resource/new-revision at limit.

  2. The new tests still do not cover the quota behavior that was requested. The only added tests are TestVideoLLMQuotaDefault and TestVideoLLMQuotaCustom (pkg/backend/video_extract_test.go:288-320), which only assert option assignment. They do not prove the 50th/51st enqueue boundary, no video_extract_visual after quota, no FFmpeg/Vision after quota, image/audio quota independence, non-allowlisted behavior, or DRIVE9_VIDEO_EXTRACT_MAX_FILES env parsing. The previous blocker was behavioral regression coverage, not just default-value coverage.

Required next fix: add behavioral tests around enqueueExtractSemanticTasksTx() or the server upload path covering N=limit-1 allows, N=limit skips next new resource, same counted resource at limit still re-enqueues for a new revision if the contract is distinct-video count, shared image/audio quota does not block video, and over-video-quota does not call extractor/worker path. Add cmd/env test coverage for DRIVE9_VIDEO_EXTRACT_MAX_FILES, including invalid/zero/negative semantics.

Validation run locally: git diff --check fcd8403..origin/pr-751 passed; focused cmd/drive9-server env tests passed; go test -c ./pkg/backend ./pkg/server ./cmd/drive9-server passed; go vet ./pkg/backend ./pkg/server ./cmd/drive9-server passed. Focused pkg/backend tests remain blocked locally by the known rootless-Docker TestMain panic. GitHub ci and drive9-server-local e2e smoke were still in progress at review time; CodeRabbit was green.

- videoLLMQuotaExceededTx now excludes the current fileID so re-uploads
  of existing videos are never blocked by the quota (only new distinct
  files count toward the limit).
- Add env tests for DRIVE9_VIDEO_EXTRACT_MAX_FILES default (50) and
  custom value parsing.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

REVISE at exact head 673a7142ca712bb288457f1bb8c0ab446816af5c.

Findings:

  1. Missing regression coverage for the actual video quota behavior. The production gate changed in pkg/backend/semantic_tasks.go:87-93 and pkg/backend/quota.go:381-390, but the new tests in cmd/drive9-server/video_extract_env_test.go:86-115 only prove env default/custom mapping, and pkg/backend/video_extract_test.go:288-318 only checks option initialization. They do not exercise a tenant at the limit through the enqueue/upload path, so they would not catch the off-by-one and current-file overwrite bugs this review has been blocking on. Required: behavioral tests proving limit-1/limit behavior, same fileID at full quota re-enqueues, 51st distinct video is skipped/no video_extract_visual, and shared image/audio quota exhaustion does not block video.

  2. Missing fail-closed/cost-path assertion for over-quota. The expected contract is upload succeeds but no video extraction cost is incurred. Add a test around enqueueExtractSemanticTasksTx or a higher upload path proving over-video-quota creates no video_extract_visual task and cannot reach the extractor/Vision path. Without that, this is config coverage, not quota enforcement coverage.

What changed:

  • B1 code logic looks fixed: videoLLMQuotaExceededTx now excludes the current fileID and uses count >= max.
  • B2 env tests for DRIVE9_VIDEO_EXTRACT_MAX_FILES default/custom are present.
  • The remaining blocker is behavior-level coverage for the quota path itself.

Validation:

  • git diff --check 46d0237..origin/pr-751 passed.
  • go test ./cmd/drive9-server -run 'TestBuildVideoExtractOptionsFromEnv|TestBuildBackendOptionsFromEnvAudioDisabled|TestBuildAudioExtractOptionsFromEnv|TestBuildBackendOptionsFromEnvAudio' -count=1 passed.
  • go test -c ./pkg/backend ./pkg/server ./cmd/drive9-server passed.
  • go vet ./pkg/backend ./pkg/server ./cmd/drive9-server passed.
  • Focused pkg/backend tests are still locally blocked by the known rootless Docker not found TestMain.
  • Hosted ci and drive9-server-local e2e smoke were pending; CodeRabbit was green.

Verdict: REVISE. Do not merge until the quota behavior itself is tested.

TestVideoQuotaSkipsEnqueueAtLimit verifies:
- First video (within quota) enqueues and succeeds
- Second distinct video (exceeds quota) uploads OK but no task created
- Re-upload of existing video at full quota still re-enqueues (fileID
  excluded from count)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

@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: 2

🧹 Nitpick comments (1)
cmd/drive9-server/video_extract_env_test.go (1)

31-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use t.Errorf for assertion failures.

As per coding guidelines, use t.Fatal/t.Fatalf for setup failures and t.Errorf for assertion failures. Since these checks are validating the final outcome of the test rather than a prerequisite setup step, they should be changed to use t.Errorf.

  • cmd/drive9-server/video_extract_env_test.go#L31-L33: Change t.Fatal to t.Errorf.
  • cmd/drive9-server/video_extract_env_test.go#L55-L57: Change t.Fatalf to t.Errorf.
  • cmd/drive9-server/video_extract_env_test.go#L69-L71: Change t.Fatal to t.Errorf.
  • cmd/drive9-server/video_extract_env_test.go#L81-L83: Change t.Fatal to t.Errorf.
  • cmd/drive9-server/video_extract_env_test.go#L97-L99: Change t.Fatalf to t.Errorf.
  • cmd/drive9-server/video_extract_env_test.go#L113-L115: Change t.Fatalf to t.Errorf.
🤖 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 `@cmd/drive9-server/video_extract_env_test.go` around lines 31 - 33, Replace
the assertion-failure calls in cmd/drive9-server/video_extract_env_test.go at
lines 31-33, 55-57, 69-71, 81-83, 97-99, and 113-115: use t.Errorf instead of
t.Fatal or t.Fatalf, preserving each existing assertion message and arguments.

Source: Coding guidelines

🤖 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 `@pkg/backend/quota.go`:
- Around line 381-391: Update Dat9Backend.videoLLMQuotaExceededTx to accept
context.Context as its first parameter, use QueryRowContext with that context,
and pass it to the warning logger instead of backgroundWithTrace(). Update the
existing caller in semantic_tasks.go to forward its available context.

In `@pkg/server/upload_test.go`:
- Around line 744-746: Change the task-count assertion for tasks2 in the upload
test from t.Fatalf to t.Errorf, preserving the existing failure message and
allowing subsequent re-upload assertions to run.

---

Nitpick comments:
In `@cmd/drive9-server/video_extract_env_test.go`:
- Around line 31-33: Replace the assertion-failure calls in
cmd/drive9-server/video_extract_env_test.go at lines 31-33, 55-57, 69-71, 81-83,
97-99, and 113-115: use t.Errorf instead of t.Fatal or t.Fatalf, preserving each
existing assertion message and arguments.
🪄 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: eb6d3aa0-93e3-4894-ad99-2d4dd2d321f6

📥 Commits

Reviewing files that changed from the base of the PR and between ad4df5b and 6af6397.

📒 Files selected for processing (8)
  • cmd/drive9-server/main.go
  • cmd/drive9-server/video_extract_env_test.go
  • pkg/backend/dat9.go
  • pkg/backend/options.go
  • pkg/backend/quota.go
  • pkg/backend/semantic_tasks.go
  • pkg/backend/video_extract_test.go
  • pkg/server/upload_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/backend/dat9.go
  • cmd/drive9-server/main.go
  • pkg/backend/options.go
  • pkg/backend/video_extract_test.go
  • pkg/backend/semantic_tasks.go

Comment thread pkg/backend/quota.go Outdated
Comment on lines +381 to +391
func (b *Dat9Backend) videoLLMQuotaExceededTx(tx *sql.Tx, fileID string) bool {
if b.maxVideoLLMFiles <= 0 {
return false
}
var count int64
if err := tx.QueryRow(`SELECT COUNT(DISTINCT resource_id) FROM semantic_tasks WHERE task_type = 'video_extract_visual' AND resource_id != ?`, fileID).Scan(&count); err != nil {
logger.Warn(backgroundWithTrace(), "video_llm_quota_check_fail_open", zap.Error(err))
return false
}
return count >= b.maxVideoLLMFiles
}

Copy link
Copy Markdown

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

Pass context.Context as the first parameter to database calls.

As per coding guidelines, context.Context must be passed as the first parameter to all I/O and database calls. This enables proper cancellation and request tracing. Please update the function signature to accept ctx and use it with QueryRowContext and the logger, rather than falling back to backgroundWithTrace().

Note: The downstream caller in pkg/backend/semantic_tasks.go already has a context available and will need to be updated to pass it.

🛠️ Proposed fix to propagate context
-func (b *Dat9Backend) videoLLMQuotaExceededTx(tx *sql.Tx, fileID string) bool {
+func (b *Dat9Backend) videoLLMQuotaExceededTx(ctx context.Context, tx *sql.Tx, fileID string) bool {
 	if b.maxVideoLLMFiles <= 0 {
 		return false
 	}
 	var count int64
-	if err := tx.QueryRow(`SELECT COUNT(DISTINCT resource_id) FROM semantic_tasks WHERE task_type = 'video_extract_visual' AND resource_id != ?`, fileID).Scan(&count); err != nil {
-		logger.Warn(backgroundWithTrace(), "video_llm_quota_check_fail_open", zap.Error(err))
+	if err := tx.QueryRowContext(ctx, `SELECT COUNT(DISTINCT resource_id) FROM semantic_tasks WHERE task_type = 'video_extract_visual' AND resource_id != ?`, fileID).Scan(&count); err != nil {
+		logger.Warn(ctx, "video_llm_quota_check_fail_open", zap.Error(err))
 		return false
 	}
 	return count >= b.maxVideoLLMFiles
 }
🤖 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 `@pkg/backend/quota.go` around lines 381 - 391, Update
Dat9Backend.videoLLMQuotaExceededTx to accept context.Context as its first
parameter, use QueryRowContext with that context, and pass it to the warning
logger instead of backgroundWithTrace(). Update the existing caller in
semantic_tasks.go to forward its available context.

Source: Coding guidelines

Comment thread pkg/server/upload_test.go
@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

REVISE at head 6af6397c0acf6ac71f131b2ecd273f6b258e263a after clarified quota contract.

New requirement from qiffang: repeated video uploads should consume repeated semantic-search quota. Example: tenant default quota 50, the same video uploaded 50 times should exhaust the quota.

Current implementation does not match that contract:

  • pkg/backend/quota.go:377-390 counts COUNT(DISTINCT resource_id) and explicitly excludes the current fileID, so repeated overwrites of the same file do not consume additional quota.
  • pkg/server/upload_test.go:748-759 now asserts the opposite behavior: with MaxVideoLLMFiles=1, re-uploading /first.mp4 still enqueues revision 2 successfully after quota is full.

If the intended product contract is per extraction/upload attempt rather than per distinct video file, this PR needs a quota model/test update before GREEN. Keep upload success behavior if desired, but the over-quota path must not enqueue FFmpeg/Vision, and tests should prove same-file repeated uploads exhaust/skip exactly according to the new rule.

Per @qiffang's decision (option 2): each Vision API call consumes one
quota unit, including re-extractions of the same file. This prevents
abuse via repeated overwrites of the same video path.

- COUNT(DISTINCT resource_id) → COUNT(*)
- Remove resource_id exclusion from quota check
- Update test: re-upload of same file now blocked when quota exhausted
- Update option comment to reflect per-extraction semantics

Co-Authored-By: Claude Opus 4.6 <[email protected]>

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/backend/quota.go (1)

381-391: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Pass context.Context as the first parameter to database calls.

As per coding guidelines, context.Context must be passed as the first parameter to all I/O and database calls. This enables proper cancellation and request tracing. Please update the function signature to accept ctx and use it with QueryRowContext and the logger, rather than falling back to backgroundWithTrace().

Note: The downstream caller in pkg/backend/semantic_tasks.go will need to be updated to pass its available context.

🛠️ Proposed fix to propagate context
-func (b *Dat9Backend) videoLLMQuotaExceededTx(tx *sql.Tx) bool {
+func (b *Dat9Backend) videoLLMQuotaExceededTx(ctx context.Context, tx *sql.Tx) bool {
 	if b.maxVideoLLMFiles <= 0 {
 		return false
 	}
 	var count int64
-	if err := tx.QueryRow(`SELECT COUNT(*) FROM semantic_tasks WHERE task_type = 'video_extract_visual'`).Scan(&count); err != nil {
-		logger.Warn(backgroundWithTrace(), "video_llm_quota_check_fail_open", zap.Error(err))
+	if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM semantic_tasks WHERE task_type = 'video_extract_visual'`).Scan(&count); err != nil {
+		logger.Warn(ctx, "video_llm_quota_check_fail_open", zap.Error(err))
 		return false
 	}
 	return count >= b.maxVideoLLMFiles
 }
🤖 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 `@pkg/backend/quota.go` around lines 381 - 391, Update
Dat9Backend.videoLLMQuotaExceededTx to accept context.Context as its first
parameter, use that context with QueryRowContext and the quota-check warning
logger, and remove the backgroundWithTrace fallback. Update the downstream
caller in semantic_tasks.go to pass its available context while preserving the
existing quota behavior.

Source: Coding guidelines

♻️ Duplicate comments (1)
pkg/server/upload_test.go (1)

744-746: 📐 Maintainability & Code Quality | 🟡 Minor | 💤 Low value

Use t.Errorf for assertion failures. As per coding guidelines, use t.Fatal/t.Fatalf for setup failures and t.Errorf for assertion failures so the test can report multiple failures before aborting.

  • pkg/server/upload_test.go#L744-L746: change t.Fatalf to t.Errorf for this task count assertion.
  • pkg/server/upload_test.go#L765-L767: change t.Fatalf to t.Errorf for this task count assertion.
🤖 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 `@pkg/server/upload_test.go` around lines 744 - 746, In the task-count
assertions in pkg/server/upload_test.go at lines 744-746 and 765-767, replace
t.Fatalf with t.Errorf while preserving the existing failure messages and
checks, so both assertion failures can be reported without aborting the test.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@pkg/backend/quota.go`:
- Around line 381-391: Update Dat9Backend.videoLLMQuotaExceededTx to accept
context.Context as its first parameter, use that context with QueryRowContext
and the quota-check warning logger, and remove the backgroundWithTrace fallback.
Update the downstream caller in semantic_tasks.go to pass its available context
while preserving the existing quota behavior.

---

Duplicate comments:
In `@pkg/server/upload_test.go`:
- Around line 744-746: In the task-count assertions in pkg/server/upload_test.go
at lines 744-746 and 765-767, replace t.Fatalf with t.Errorf while preserving
the existing failure messages and checks, so both assertion failures can be
reported without aborting the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec6238a6-a20c-40de-8196-4df8a5066f80

📥 Commits

Reviewing files that changed from the base of the PR and between 6af6397 and 490e465.

📒 Files selected for processing (4)
  • pkg/backend/options.go
  • pkg/backend/quota.go
  • pkg/backend/semantic_tasks.go
  • pkg/server/upload_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/backend/semantic_tasks.go

@qiffang

qiffang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

GREEN at exact head 490e4653e40561758e640b61cebfcb3588549959.

No remaining blocking findings from my gate.

The final quota blocker is fixed against qiffang's option-2 contract: video quota is now per extraction attempt / Vision call, not per distinct file. videoLLMQuotaExceededTx counts COUNT(*) over video_extract_visual task rows, no longer excludes the current file_id, and uses the correct pre-enqueue count >= max boundary. The behavior test now locks the critical case: with MaxVideoLLMFiles=1, the first upload enqueues, a second distinct video skips extraction while upload succeeds, and a same-file re-upload also skips extraction instead of bypassing quota.

Validation:

  • git diff --check bfd070e48de4002e50f4f09e25fd4a2245a3109d..HEAD pass
  • go test ./cmd/drive9-server -run TestBuildVideoExtractOptionsFromEnv -count=1 pass
  • go test -c ./pkg/backend ./pkg/server ./cmd/drive9-server pass
  • go vet ./pkg/backend ./pkg/server ./cmd/drive9-server pass
  • GitHub ci pass at this head
  • GitHub drive9-server-local e2e smoke pass at this head
  • CodeRabbit success at this head

Local focused pkg/backend / pkg/server runtime tests are still blocked on this machine by the known rootless Docker not found TestMain, so hosted CI/e2e is the runtime execution evidence.

Scope boundary remains unchanged: GREEN covers the default-off / tenant-allowlisted video visual extraction pipeline, Docker FFmpeg dependency, and per-extraction video quota. It is not proof of a real Bailian/DashScope Vision smoke with production credentials and final content/search quality.

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