mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
Classificação automática com proveniência (job + CLI recordings:classify), facetas ano/tema/alcance/município/duração na URL canônica, busca por tema no corpus próprio, ordenação e a barra de filtros portada do design aprovado sobre os shells compartilhados. Deep-links existentes byte-idênticos; Câmara intocada.
162 lines
5.7 KiB
JavaScript
162 lines
5.7 KiB
JavaScript
/**
|
|
* C219 — facet backfill of the uploaded recordings ("Gravações enviadas").
|
|
*
|
|
* The transcription job classifies every recording it finishes; this CLI
|
|
* classifies the rows that already existed (and can reclassify on demand) with
|
|
* the SAME function the job uses (`classifyRecordingFacets` → `classifySpeech`:
|
|
* offline gazetteer + one validated LLM refinement on Deep Infra). It never
|
|
* invents provenance: a row is only written when the classifier produced a
|
|
* result, a `manual` curation is never touched, and the default selection is
|
|
* exactly the rows with no `classifiedBy` (never classified).
|
|
*
|
|
* Idempotent: re-running skips what it already classified (default mode);
|
|
* `--all` reclassifies everything except `manual` rows.
|
|
*
|
|
* The local-DB guard refuses a non-local DATABASE_URL, and any write run
|
|
* targeting production (or a remote/override DB) additionally requires
|
|
* `RECORDING_CLASSIFY_CONFIRM=1` (same family as `CAMARA_IMPORT_CONFIRM`).
|
|
*
|
|
* Usage:
|
|
* pnpm recordings:classify --dry-run
|
|
* pnpm recordings:classify --limit 5
|
|
* pnpm recordings:classify
|
|
* pnpm recordings:classify --all
|
|
* RECORDING_CLASSIFY_CONFIRM=1 pnpm recordings:classify --all
|
|
*/
|
|
import { getPayload } from 'payload'
|
|
|
|
import { assertLocalDatabase } from './assert-local-database.mjs'
|
|
import { dieWithLabel, isTruthyEnv, loadCliEnv, requiresWriteConfirm } from './lib/cli.mjs'
|
|
|
|
loadCliEnv()
|
|
|
|
const die = dieWithLabel('recordings:classify')
|
|
const WRITE_CONFIRM_FLAG = 'RECORDING_CLASSIFY_CONFIRM'
|
|
|
|
const parseArgs = (argv) => {
|
|
const options = { dryRun: false, all: false, limit: null }
|
|
for (const arg of argv) {
|
|
if (arg === '--dry-run') options.dryRun = true
|
|
else if (arg === '--all') options.all = true
|
|
else if (arg.startsWith('--limit=')) {
|
|
const limit = Number(arg.slice('--limit='.length))
|
|
if (!Number.isSafeInteger(limit) || limit <= 0) die(`--limit inválido: ${arg}`)
|
|
options.limit = limit
|
|
} else die(`argumento desconhecido: ${arg}`)
|
|
}
|
|
return options
|
|
}
|
|
|
|
const options = parseArgs(process.argv.slice(2))
|
|
|
|
assertLocalDatabase(
|
|
'recordings:classify',
|
|
'A classificação escreve em `recording`; aponte DATABASE_URL para o banco local.',
|
|
)
|
|
|
|
if (requiresWriteConfirm() && !isTruthyEnv(process.env[WRITE_CONFIRM_FLAG])) {
|
|
die(
|
|
`Este run escreve em um banco remoto/de produção. Se é isso mesmo, re-execute com:\n ${WRITE_CONFIRM_FLAG}=1 pnpm recordings:classify …`,
|
|
)
|
|
}
|
|
|
|
const config = (await import('../src/payload.config.ts')).default
|
|
const { classifyRecordingFacets, recordingFacetWriteData } =
|
|
await import('../src/utilities/recordings/recordingClassification.ts')
|
|
|
|
const payload = await getPayload({ config })
|
|
|
|
const where = options.all
|
|
? { and: [{ status: { equals: 'ready' } }, { classifiedBy: { not_equals: 'manual' } }] }
|
|
: { and: [{ status: { equals: 'ready' } }, { classifiedBy: { exists: false } }] }
|
|
|
|
const targets = await payload.find({
|
|
collection: 'recording',
|
|
where,
|
|
depth: 0,
|
|
limit: options.limit ?? 0,
|
|
pagination: false,
|
|
sort: 'createdAt',
|
|
select: { title: true, classifiedBy: true },
|
|
// Intentional admin bypass: the backfill CLI is a trusted actor with no session.
|
|
overrideAccess: true,
|
|
})
|
|
|
|
const report = {
|
|
targets: targets.docs.length,
|
|
classified: 0,
|
|
skippedEmptyTranscript: 0,
|
|
failed: 0,
|
|
llmUsed: 0,
|
|
totalTokens: 0,
|
|
estimatedCostUsd: 0,
|
|
}
|
|
|
|
console.log(
|
|
`[recordings:classify] ${report.targets} gravação(ões) alvo${options.dryRun ? ' (dry-run)' : ''}.`,
|
|
)
|
|
|
|
for (const recording of targets.docs) {
|
|
const segments = await payload.find({
|
|
collection: 'recordingSegment',
|
|
where: { recording: { equals: recording.id } },
|
|
depth: 0,
|
|
limit: 0,
|
|
pagination: false,
|
|
sort: 'order',
|
|
select: { text: true },
|
|
// Intentional admin bypass: reconstructing the transcript the job owns.
|
|
overrideAccess: true,
|
|
})
|
|
const transcript = segments.docs.map((segment) => segment.text).join(' ')
|
|
|
|
const classification = await classifyRecordingFacets({ payload, transcript })
|
|
if (!classification) {
|
|
report.skippedEmptyTranscript += 1
|
|
console.warn(`[recordings:classify] #${recording.id} sem transcrição utilizável — pulada.`)
|
|
continue
|
|
}
|
|
|
|
if (classification.llm.used) report.llmUsed += 1
|
|
if (classification.llm.totalTokens !== null) report.totalTokens += classification.llm.totalTokens
|
|
if (classification.llm.estimatedCostUsd !== null) {
|
|
report.estimatedCostUsd += classification.llm.estimatedCostUsd
|
|
}
|
|
|
|
if (options.dryRun) {
|
|
report.classified += 1
|
|
console.log(
|
|
`[recordings:classify] (dry-run) #${recording.id} "${recording.title}" → ` +
|
|
`${classification.topics.join(', ') || '—'} · ${classification.classifiedBy}`,
|
|
)
|
|
continue
|
|
}
|
|
|
|
try {
|
|
await payload.update({
|
|
collection: 'recording',
|
|
id: recording.id,
|
|
data: recordingFacetWriteData(classification),
|
|
// Intentional admin bypass: the backfill CLI is a trusted actor with no session.
|
|
overrideAccess: true,
|
|
})
|
|
report.classified += 1
|
|
console.log(
|
|
`[recordings:classify] #${recording.id} "${recording.title}" → ` +
|
|
`${classification.topics.join(', ') || '—'} · ${classification.classifiedBy}`,
|
|
)
|
|
} catch (error) {
|
|
report.failed += 1
|
|
console.warn(
|
|
`[recordings:classify] #${recording.id} falhou ao gravar: ` +
|
|
`${error instanceof Error ? error.message : String(error)}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`\n[recordings:classify] alvos=${report.targets} classificadas=${report.classified} ` +
|
|
`sem-transcrição=${report.skippedEmptyTranscript} falhas=${report.failed} ` +
|
|
`llm=${report.llmUsed} tokens=${report.totalTokens} custo≈$${report.estimatedCostUsd.toFixed(4)}`,
|
|
)
|
|
process.exit(report.failed > 0 ? 1 : 0)
|