@gentbajko/slopify 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +11 -1
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +55 -5
- package/dist/adapters/host-cli/transport.js +13 -6
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +60 -18
- package/dist/adapters/image/codex.js +166 -25
- package/dist/adapters/image/fal.js +31 -3
- package/dist/adapters/image/google.js +17 -2
- package/dist/adapters/image/openai.js +45 -16
- package/dist/adapters/image/reference.js +41 -0
- package/dist/adapters/image/replicate.js +14 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +26 -9
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +7 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +33 -12
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +12 -1
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +17 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +45 -6
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +128 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +12 -3
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +139 -16
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +18 -9
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +164 -9
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +321 -20
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +35 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +126 -1
- package/dist/slices/admission/schema.js +68 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +90 -16
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +43 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +9 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +113 -24
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +15 -3
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +94 -0
- package/dist/slices/notifications/rules.js +102 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +160 -9
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +9 -1
- package/dist/slices/play-drafts/review-inputs.js +22 -3
- package/dist/slices/play-drafts/schema.js +109 -2
- package/dist/slices/play-drafts/service.js +10 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +70 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +29 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +67 -2
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +15 -5
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +43 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +58 -0
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +32 -5
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +29 -3
- package/dist/slices/rebuild/recipe-visual.js +104 -29
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +14 -2
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +69 -0
- package/dist/slices/rebuild/runtime-local.js +154 -27
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +29 -0
- package/dist/slices/rebuild/runtime-provider.js +134 -19
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +48 -23
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +10 -3
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +12 -2
- package/dist/slices/revisions/adopt.js +9 -2
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +43 -9
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-prepare.js +8 -10
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +16 -2
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +48 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +11 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +20 -0
- package/dist/slices/storage/model.js +14 -0
- package/dist/slices/storage/portable.js +95 -12
- package/dist/slices/storage/reconcile.js +19 -2
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-BGgYMm2u.css +0 -1
- package/dist/web/assets/index-BVVgtntL.js +0 -171
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { subtitleModelDir } from "../../kernel/paths.js";
|
|
4
|
+
import { projectLanguage } from "../../kernel/ports/languages.js";
|
|
4
5
|
import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
|
|
6
|
+
import { narrationAliasesOf } from "../admission/rules.js";
|
|
7
|
+
import { captionFont, captionFontDeps } from "../fonts/coverage.js";
|
|
5
8
|
import { resolveFont } from "../fonts/index.js";
|
|
6
9
|
import { validateCues } from "../revisions/rules.js";
|
|
7
10
|
import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
|
|
8
11
|
import { outputPath } from "../storage/layout.js";
|
|
9
12
|
import { captionCues, serializeAss, serializeSrt, serializeVtt } from "../subtitles/captions.js";
|
|
10
13
|
import { spoken } from "../video/plan.js";
|
|
11
|
-
import {
|
|
14
|
+
import { usesVoices } from "../voices/model.js";
|
|
15
|
+
import { speakerColour } from "../voices/palette.js";
|
|
16
|
+
import { speakerPanelEvents, usesSpeakerPanel } from "../voices/panel.js";
|
|
17
|
+
import { panelPortraits } from "../voices/portraits.js";
|
|
18
|
+
import { attributeWords } from "../voices/timing.js";
|
|
19
|
+
import { exportSnapshot, retainedOutput, revisionAudio, revisionTranscript, revisionTurns, } from "./runtime-export-inputs.js";
|
|
12
20
|
import { narrationChunks, narrationTextParts, } from "./runtime-narration-text.js";
|
|
13
21
|
import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
|
|
14
22
|
// The saved `subtitles.json`: every word at its time in the final video. The YouTube
|
|
@@ -20,6 +28,9 @@ export const wordsSchema = z.object({
|
|
|
20
28
|
start: z.number().finite().nonnegative(),
|
|
21
29
|
end: z.number().finite().positive(),
|
|
22
30
|
confidence: z.number().optional(),
|
|
31
|
+
// A multi-voice run's speaker id and script turn (`voices/timing.ts`).
|
|
32
|
+
speaker: z.string().optional(),
|
|
33
|
+
turn: z.number().int().positive().optional(),
|
|
23
34
|
})),
|
|
24
35
|
omissions: z
|
|
25
36
|
.array(z.object({ start: z.number().finite().nonnegative(), text: z.string() }))
|
|
@@ -30,6 +41,7 @@ const cuesSchema = z.object({
|
|
|
30
41
|
text: z.string(),
|
|
31
42
|
start: z.number().finite().nonnegative(),
|
|
32
43
|
end: z.number().finite().positive(),
|
|
44
|
+
speaker: z.string().optional(),
|
|
33
45
|
})),
|
|
34
46
|
omissions: wordsSchema.shape.omissions,
|
|
35
47
|
});
|
|
@@ -44,19 +56,21 @@ export async function executeSubtitleRecipe(deps, context, piece) {
|
|
|
44
56
|
return cues(deps, context, piece, snapshot);
|
|
45
57
|
if (piece.key === "subtitles:files")
|
|
46
58
|
return files(deps, context, piece, snapshot);
|
|
47
|
-
throw new Error("Slopify hit an internal error (unknown kind of caption step).
|
|
59
|
+
throw new Error("Slopify hit an internal error (unknown kind of caption step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
48
60
|
}
|
|
49
61
|
async function timing(deps, context, piece, snapshot) {
|
|
50
62
|
const audio = await revisionAudio(deps, context, snapshot.view);
|
|
51
63
|
if (audio.length === 0)
|
|
52
|
-
throw new Error("Captions and the YouTube description need narration audio, but this project has none. Turn captions and the YouTube description off in Edit project, or turn narration on, then
|
|
64
|
+
throw new Error("Captions and the YouTube description need narration audio, but this project has none. Turn captions and the YouTube description off in Edit project, or turn narration on, then Try again.");
|
|
53
65
|
const alignSubtitles = deps.alignSubtitles;
|
|
54
66
|
if (alignSubtitles === undefined)
|
|
55
|
-
throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build).
|
|
67
|
+
throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
56
68
|
if (!context.maySubmit(piece.id))
|
|
57
69
|
return "held";
|
|
58
70
|
const words = [];
|
|
59
71
|
const omissions = [];
|
|
72
|
+
const turns = revisionTurns(snapshot);
|
|
73
|
+
const language = projectLanguage(snapshot.view.revision.config);
|
|
60
74
|
let offset = 0;
|
|
61
75
|
const total = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
62
76
|
for (const segment of audio) {
|
|
@@ -66,7 +80,9 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
66
80
|
const aligned = await located(snapshot, kind, () => alignSubtitles({
|
|
67
81
|
audioPath: path,
|
|
68
82
|
text: revisionTranscript(deps, snapshot, kind),
|
|
69
|
-
|
|
83
|
+
aliases: narrationAliasesOf(snapshot.view.revision.config),
|
|
84
|
+
cacheDir: subtitleModelDir(deps.paths.dataDir, language),
|
|
85
|
+
...(language === "en" ? {} : { language }),
|
|
70
86
|
ffmpeg: deps.ffmpeg,
|
|
71
87
|
signal: context.signal,
|
|
72
88
|
onOmission: (value) => omissions.push({ ...value, start: value.start + offset }),
|
|
@@ -78,9 +94,11 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
78
94
|
total: 100,
|
|
79
95
|
}),
|
|
80
96
|
}));
|
|
81
|
-
for (const word of
|
|
97
|
+
for (const word of kind === "body" && turns.length > 0
|
|
98
|
+
? attributeWords(turns, aligned)
|
|
99
|
+
: aligned) {
|
|
82
100
|
if (word.end > segment.seconds + 0.1)
|
|
83
|
-
throw new Error("Caption timing came out longer than the narration audio.
|
|
101
|
+
throw new Error("Caption timing came out longer than the narration audio. Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
84
102
|
words.push({
|
|
85
103
|
...word,
|
|
86
104
|
start: word.start + offset,
|
|
@@ -91,7 +109,7 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
91
109
|
offset += segment.seconds;
|
|
92
110
|
}
|
|
93
111
|
if (captionCues(words).length === 0)
|
|
94
|
-
throw new Error("None of the narration could be matched to the article text, so captions can't be timed.
|
|
112
|
+
throw new Error("None of the narration could be matched to the article text, so captions can't be timed. The narration must be in the project language set in Edit project → Language; if you uploaded your own audio, make sure it reads the article text, then Try again.");
|
|
95
113
|
context.signal.throwIfAborted();
|
|
96
114
|
const output = preparedText(deps, context, piece, "subtitle_words", "subtitles.json", JSON.stringify({ key: piece.logicalFingerprint ?? piece.fingerprint, words, omissions }));
|
|
97
115
|
await publishResult(deps, context, piece, [output], { words, omissions }, output.asset);
|
|
@@ -103,7 +121,7 @@ async function cues(deps, context, piece, snapshot) {
|
|
|
103
121
|
let value;
|
|
104
122
|
if (manual !== undefined) {
|
|
105
123
|
if (plan.work.find((one) => one.key === piece.key)?.disposition === "review")
|
|
106
|
-
throw new Error("Your edited captions were written for an older version of the narration. Review them in Edit project → Captions and save, then
|
|
124
|
+
throw new Error("Your edited captions were written for an older version of the narration. Review them in Edit project → Captions and save, then Try again.");
|
|
107
125
|
const audio = await revisionAudio(deps, context, view);
|
|
108
126
|
const errors = validateCues(manual.cues, audio.reduce((sum, segment) => sum + segment.seconds, 0));
|
|
109
127
|
if (errors.length > 0)
|
|
@@ -116,7 +134,7 @@ async function cues(deps, context, piece, snapshot) {
|
|
|
116
134
|
one.state === "ready" &&
|
|
117
135
|
one.workKey === "subtitles:timing");
|
|
118
136
|
if (output === undefined)
|
|
119
|
-
throw new Error("Caption timing is missing. Use
|
|
137
|
+
throw new Error("Caption timing is missing. Use More → Render the video again in the Video section, then Try again.");
|
|
120
138
|
const timed = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, output.output.path), "utf8")));
|
|
121
139
|
value = { cues: [...captionCues(timed.words)], omissions: timed.omissions };
|
|
122
140
|
}
|
|
@@ -130,10 +148,10 @@ async function files(deps, context, piece, snapshot) {
|
|
|
130
148
|
const { view } = snapshot;
|
|
131
149
|
const config = view.revision.config.subtitles;
|
|
132
150
|
if (config === undefined || config.mode === "off")
|
|
133
|
-
throw new Error("Slopify hit an internal error (captions are off for this version of the project).
|
|
151
|
+
throw new Error("Slopify hit an internal error (captions are off for this version of the project). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
134
152
|
const row = view.pieces.find((one) => one.key === "subtitles:cues" && one.selected && one.available && one.piece.state === "done");
|
|
135
153
|
if (row?.piece.payload === null || row?.piece.payload === undefined)
|
|
136
|
-
throw new Error("The captions haven't been prepared yet. Use
|
|
154
|
+
throw new Error("The captions haven't been prepared yet. Use More → Render the video again in the Video section, then Try again.");
|
|
137
155
|
const value = cuesSchema.parse(JSON.parse(row.piece.payload));
|
|
138
156
|
const previous = view.outputs.find((one) => one.output.role === "subtitle_font" && one.selected && one.available);
|
|
139
157
|
const previousFiles = view.pieces.find((one) => one.key === "subtitles:files" &&
|
|
@@ -152,9 +170,17 @@ async function files(deps, context, piece, snapshot) {
|
|
|
152
170
|
.optional(),
|
|
153
171
|
})
|
|
154
172
|
.parse(JSON.parse(previousFiles?.piece.payload ?? "{}")).font;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
// Another language's captions may need a font with its letters (`fonts/coverage.ts`); an
|
|
174
|
+
// English project keeps the saved font as it always did.
|
|
175
|
+
const language = projectLanguage(view.revision.config);
|
|
176
|
+
const font = language !== "en"
|
|
177
|
+
? await captionFont(captionFontDeps(deps.paths), config.fontId, language)
|
|
178
|
+
: savedFont?.id === config.fontId && previous !== undefined
|
|
179
|
+
? {
|
|
180
|
+
...savedFont,
|
|
181
|
+
path: outputPath(deps.paths, context.work.projectId, previous.output.path),
|
|
182
|
+
}
|
|
183
|
+
: await resolveFont(deps.paths, config.fontId);
|
|
158
184
|
if (!context.maySubmit(piece.id))
|
|
159
185
|
return "held";
|
|
160
186
|
const prepared = [];
|
|
@@ -162,9 +188,20 @@ async function files(deps, context, piece, snapshot) {
|
|
|
162
188
|
const frame = view.revision.config.format === "16:9"
|
|
163
189
|
? { width: 1920, height: 1080 }
|
|
164
190
|
: { width: 1080, height: 1920 };
|
|
191
|
+
const voices = captionVoices(view.revision.config);
|
|
192
|
+
const portraits = panelPortraits(deps.db, voices);
|
|
193
|
+
const overlay = voices !== undefined && usesSpeakerPanel(voices.format)
|
|
194
|
+
? speakerPanelEvents(value.cues, voices.speakers.map((speaker, index) => ({
|
|
195
|
+
id: speaker.id,
|
|
196
|
+
name: speaker.name.trim(),
|
|
197
|
+
colour: speakerColour(index),
|
|
198
|
+
...(portraits[index] === undefined ? {} : { portrait: true }),
|
|
199
|
+
})), frame, (await revisionAudio(deps, context, view)).reduce((sum, segment) => sum + segment.seconds, 0))
|
|
200
|
+
: [];
|
|
201
|
+
const speakers = voices === undefined ? undefined : captionSpeakers(voices);
|
|
165
202
|
for (const [role, filename, text] of [
|
|
166
|
-
["subtitles_srt", "subtitles.srt", serializeSrt(value.cues)],
|
|
167
|
-
["subtitles_vtt", "subtitles.vtt", serializeVtt(value.cues)],
|
|
203
|
+
["subtitles_srt", "subtitles.srt", serializeSrt(value.cues, speakers)],
|
|
204
|
+
["subtitles_vtt", "subtitles.vtt", serializeVtt(value.cues, speakers)],
|
|
168
205
|
[
|
|
169
206
|
"subtitle_ass",
|
|
170
207
|
"subtitles.ass",
|
|
@@ -173,6 +210,10 @@ async function files(deps, context, piece, snapshot) {
|
|
|
173
210
|
fontName: font.assName,
|
|
174
211
|
fontSize: config.fontSize,
|
|
175
212
|
position: config.position,
|
|
213
|
+
color: config.color,
|
|
214
|
+
outlineColor: config.outlineColor,
|
|
215
|
+
speakers,
|
|
216
|
+
overlay,
|
|
176
217
|
}),
|
|
177
218
|
],
|
|
178
219
|
])
|
|
@@ -242,8 +283,8 @@ export function describeMismatch(chunks, segment, mismatch) {
|
|
|
242
283
|
const expected = mismatch.expected === "" ? "the end of the text" : `"${mismatch.expected}…"`;
|
|
243
284
|
const heard = mismatch.heard === "" ? "no more speech" : `"${mismatch.heard.toLowerCase()}…"`;
|
|
244
285
|
const fix = chunk === undefined
|
|
245
|
-
? "Check that part of the narration, regenerate it in Edit project → Narration, then
|
|
246
|
-
: `In Edit project → Narration, regenerate narration chunk ${String(at + 1)}, then
|
|
286
|
+
? "Check that part of the narration, regenerate it in Edit project → Narration, then Continue the run."
|
|
287
|
+
: `In Edit project → Narration, regenerate narration chunk ${String(at + 1)}, then Continue the run.`;
|
|
247
288
|
return `Subtitles stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard} The recording there probably skips or changes words. ${fix}`;
|
|
248
289
|
}
|
|
249
290
|
function comparable(text) {
|
|
@@ -266,3 +307,15 @@ function clock(seconds) {
|
|
|
266
307
|
? `${String(hours)}:${String(minutes).padStart(2, "0")}:${rest}`
|
|
267
308
|
: `${String(minutes)}:${rest}`;
|
|
268
309
|
}
|
|
310
|
+
function captionVoices(config) {
|
|
311
|
+
return usesVoices(config) ? config.voices : undefined;
|
|
312
|
+
}
|
|
313
|
+
function captionSpeakers(voices) {
|
|
314
|
+
return {
|
|
315
|
+
styles: Object.fromEntries(voices.speakers.map((speaker, index) => [
|
|
316
|
+
speaker.id,
|
|
317
|
+
{ name: speaker.name.trim(), colour: speakerColour(index) },
|
|
318
|
+
])),
|
|
319
|
+
nameTags: voices.nameTags,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { masterFile, masterReport } from "../loudness/loudnorm.js";
|
|
5
|
+
import { masterGoal } from "../loudness/model.js";
|
|
6
|
+
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
7
|
+
import { outputPath, projectDir } from "../storage/layout.js";
|
|
8
|
+
import { audioExportArgs } from "../video/audio-export-args.js";
|
|
9
|
+
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
10
|
+
import { audioChapters, audioFileArgs, ffmetadata } from "../voices/audio-files.js";
|
|
11
|
+
import { turnStarts } from "../voices/timing.js";
|
|
12
|
+
import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
|
|
13
|
+
import { preparedResult, publishResult } from "./runtime-publication.js";
|
|
14
|
+
import { wordsSchema } from "./runtime-subtitles.js";
|
|
15
|
+
// The listening files of a multi-voice run: the MP3 and the M4B of the narration timeline,
|
|
16
|
+
// with a chapter at each section of the script where its first turn is heard.
|
|
17
|
+
export async function executeVoicesRecipe(deps, context, piece) {
|
|
18
|
+
if (deps.db
|
|
19
|
+
.prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
20
|
+
.get(piece.id, context.work.workId)?.state === "done")
|
|
21
|
+
return "done";
|
|
22
|
+
if (!context.maySubmit(piece.id))
|
|
23
|
+
return "held";
|
|
24
|
+
if (piece.key !== "voices:files" || piece.input.kind !== "local")
|
|
25
|
+
throw new Error("Slopify hit an internal error (unknown kind of listening-file step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
26
|
+
context.signal.throwIfAborted();
|
|
27
|
+
const { view } = exportSnapshot(deps, context, piece);
|
|
28
|
+
const sections = z
|
|
29
|
+
.tuple([z.unknown(), z.unknown(), z.array(z.tuple([z.string(), z.number()])), z.string()])
|
|
30
|
+
// Level the volume's master may follow (`recipe-loudness.ts`).
|
|
31
|
+
.rest(z.unknown())
|
|
32
|
+
.parse(piece.input.values)[2]
|
|
33
|
+
.map(([title, firstTurn]) => ({ title, firstTurn }));
|
|
34
|
+
const timing = view.outputs.find((row) => row.selected &&
|
|
35
|
+
row.available &&
|
|
36
|
+
row.state === "ready" &&
|
|
37
|
+
row.workKey === "subtitles:timing" &&
|
|
38
|
+
row.output.role === "subtitle_words");
|
|
39
|
+
if (timing === undefined)
|
|
40
|
+
throw new Error("The narration's word timing, which the chapter markers come from, is missing. Use More → Render the video again in the Video section, then Try again.");
|
|
41
|
+
const { words } = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
|
|
42
|
+
const audio = await revisionAudio(deps, context, view, { levelled: true });
|
|
43
|
+
if (audio.length === 0)
|
|
44
|
+
throw new Error("The MP3 and M4B files need narration audio, but this project has none. Turn narration on in Edit project, or turn Audio files off under Speakers (Play → Audio, or Edit project → Providers), then Try again.");
|
|
45
|
+
const totalSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
46
|
+
const chapters = audioChapters({
|
|
47
|
+
title: view.revision.config.title,
|
|
48
|
+
sections,
|
|
49
|
+
turnStarts: turnStarts(words),
|
|
50
|
+
totalSeconds,
|
|
51
|
+
});
|
|
52
|
+
const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "audio-"));
|
|
53
|
+
const pending = [
|
|
54
|
+
allocateAsset(deps, context.work.projectId, "narration.mp3"),
|
|
55
|
+
allocateAsset(deps, context.work.projectId, "audiobook.m4b"),
|
|
56
|
+
];
|
|
57
|
+
const prepared = [];
|
|
58
|
+
try {
|
|
59
|
+
const metadata = join(directory, "chapters.txt");
|
|
60
|
+
writeFileSync(metadata, ffmetadata(view.revision.config.title, chapters), { mode: 0o600 });
|
|
61
|
+
// Level the volume: the sound is mixed once and mastered to the audio files' target, and both
|
|
62
|
+
// files are encoded from that (`loudness/model.ts`).
|
|
63
|
+
const goal = masterGoal(view.revision.config, "audioFiles");
|
|
64
|
+
let sound = audio;
|
|
65
|
+
if (goal !== undefined) {
|
|
66
|
+
const mixed = join(directory, "mix.wav");
|
|
67
|
+
await runFfmpeg({
|
|
68
|
+
bin: deps.ffmpeg,
|
|
69
|
+
args: audioExportArgs(audio, mixed),
|
|
70
|
+
signal: context.signal,
|
|
71
|
+
log: deps.log,
|
|
72
|
+
onProgress: () => { },
|
|
73
|
+
});
|
|
74
|
+
const mastered = join(directory, "master.wav");
|
|
75
|
+
await masterFile({ bin: deps.ffmpeg, log: deps.log, signal: context.signal }, mixed, mastered, goal, { sampleRate: 44100, channels: 2 });
|
|
76
|
+
sound = [{ path: mastered, seconds: totalSeconds }];
|
|
77
|
+
}
|
|
78
|
+
for (const [at, kind] of ["mp3", "m4b"].entries()) {
|
|
79
|
+
const target = pending[at];
|
|
80
|
+
if (target === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
await runFfmpeg({
|
|
83
|
+
bin: deps.ffmpeg,
|
|
84
|
+
args: audioFileArgs(sound, metadata, target.absolutePath, kind),
|
|
85
|
+
signal: context.signal,
|
|
86
|
+
log: deps.log,
|
|
87
|
+
onProgress: () => { },
|
|
88
|
+
});
|
|
89
|
+
context.signal.throwIfAborted();
|
|
90
|
+
// What each finished file measures, after its encode.
|
|
91
|
+
const master = goal === undefined
|
|
92
|
+
? undefined
|
|
93
|
+
: await masterReport({ bin: deps.ffmpeg, log: deps.log, signal: context.signal }, target.absolutePath, goal);
|
|
94
|
+
prepared.push(preparedResult(deps, context, piece, kind === "mp3" ? "audio_mp3" : "audio_m4b", sealAsset(deps, target), Math.round(totalSeconds * 1000), master === undefined ? {} : { master }));
|
|
95
|
+
}
|
|
96
|
+
await publishResult(deps, context, piece, prepared, { chapters });
|
|
97
|
+
return "done";
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
discardPreparedAssets(deps, [...pending, ...prepared.map((one) => one.asset)]);
|
|
101
|
+
rmSync(directory, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { withLanguage } from "../../kernel/ports/languages.js";
|
|
2
3
|
import { outputPath } from "../storage/layout.js";
|
|
3
4
|
import { assembleDescription, checkDescriptionAnswer, descriptionMessages, tagsText, } from "../youtube/answer.js";
|
|
4
5
|
import { defaultDescriptionPrompt } from "../youtube/model.js";
|
|
@@ -21,35 +22,35 @@ export async function executeYoutubeRecipe(deps, context, providers, piece) {
|
|
|
21
22
|
context.signal.throwIfAborted();
|
|
22
23
|
const input = piece.input;
|
|
23
24
|
if (input.kind !== "local" || input.operation !== "youtube-description-v1")
|
|
24
|
-
throw new Error("Slopify hit an internal error (the YouTube description step was set up wrongly).
|
|
25
|
+
throw new Error("Slopify hit an internal error (the YouTube description step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
25
26
|
const { view } = exportSnapshot(deps, context, piece);
|
|
26
27
|
const config = view.revision.config;
|
|
27
28
|
const llm = config.llm;
|
|
28
29
|
if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
|
|
29
|
-
throw new Error("The YouTube description needs an AI text model, and none is chosen. Choose one in Edit project → Providers, then
|
|
30
|
+
throw new Error("The YouTube description needs an AI text model, and none is chosen. Choose one in Edit project → Providers, then Try again.");
|
|
30
31
|
const timing = view.outputs.find((row) => row.selected &&
|
|
31
32
|
row.available &&
|
|
32
33
|
row.state === "ready" &&
|
|
33
34
|
row.workKey === "subtitles:timing" &&
|
|
34
35
|
row.output.role === "subtitle_words");
|
|
35
36
|
if (timing === undefined)
|
|
36
|
-
throw new Error("The narration's word timing, which the YouTube description's chapters come from, is missing. Use
|
|
37
|
+
throw new Error("The narration's word timing, which the YouTube description's chapters come from, is missing. Use More → Render the video again in the Video section, then Try again.");
|
|
37
38
|
const { words } = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
|
|
38
39
|
const passages = transcriptPassages(words);
|
|
39
40
|
if (passages.length === 0)
|
|
40
|
-
throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Use
|
|
41
|
+
throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Use More → Render the video again in the Video section, then Try again.");
|
|
41
42
|
// The same timeline the timing walked: silence at the start, intro, gaps, body, outro and
|
|
42
43
|
// silence at the end.
|
|
43
44
|
const audio = await revisionAudio(deps, context, view);
|
|
44
45
|
const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
45
46
|
const saved = Array.isArray(input.values) ? input.values[4] : undefined;
|
|
46
47
|
const instruction = typeof saved === "string" && saved.trim() !== "" ? saved : defaultDescriptionPrompt;
|
|
47
|
-
const messages = descriptionMessages({
|
|
48
|
+
const messages = withLanguage(descriptionMessages({
|
|
48
49
|
instruction,
|
|
49
50
|
title: config.title,
|
|
50
51
|
durationSeconds,
|
|
51
52
|
transcript: transcriptText(passages),
|
|
52
|
-
});
|
|
53
|
+
}), config.language);
|
|
53
54
|
const row = deps.db
|
|
54
55
|
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
55
56
|
.get(context.work.workId);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkRuntimeModel } from "../../catalog/runtime-models.js";
|
|
2
|
+
import { referenceRefusal, takesReferenceImage } from "../../catalog/schema.js";
|
|
2
3
|
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
3
4
|
import { cliPathChanged } from "../settings/cli-paths.js";
|
|
4
5
|
import { isLocalCliProvider } from "../settings/model.js";
|
|
@@ -38,7 +39,7 @@ export async function checkReadiness(deps, snapshot, view) {
|
|
|
38
39
|
});
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
|
-
const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.family === "
|
|
42
|
+
const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.family === "tts" ? undefined : (choice.thinking ?? undefined));
|
|
42
43
|
if (result === "missing")
|
|
43
44
|
fields.push({
|
|
44
45
|
field: `${choice.family}.model`,
|
|
@@ -112,6 +113,19 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
|
|
|
112
113
|
field: `work.${recipe.key}.aspect`,
|
|
113
114
|
message: "The model does not support this output shape.",
|
|
114
115
|
});
|
|
116
|
+
// Only the keyed providers' models are listed; the Codex CLI always takes a reference.
|
|
117
|
+
if (model !== undefined &&
|
|
118
|
+
input.reference !== undefined &&
|
|
119
|
+
!takesReferenceImage(model) &&
|
|
120
|
+
!fields.some((row) => row.field === "reference.source"))
|
|
121
|
+
fields.push({ field: "reference.source", message: referenceRefusal });
|
|
122
|
+
if (model !== undefined &&
|
|
123
|
+
input.thinking !== undefined &&
|
|
124
|
+
!fields.some((row) => row.field === "images.thinking"))
|
|
125
|
+
fields.push({
|
|
126
|
+
field: "images.thinking",
|
|
127
|
+
message: "This image model has no effort setting. Set the images' Effort back to Model default under Providers.",
|
|
128
|
+
});
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
return fields;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Soften and retry: an image prompt a content filter refused is reworded by the project's
|
|
2
|
+
// AI model, inside the image step's own run (so the call goes through the retry policy and
|
|
3
|
+
// the new wording streams onto the step's live panel), then drawn again. A request is kept
|
|
4
|
+
// per step until an image is made from the softened prompt.
|
|
5
|
+
// The steps of a stage whose last attempt a content filter refused, on the current run.
|
|
6
|
+
export function refusedKeys(db, projectId, stage) {
|
|
7
|
+
return db
|
|
8
|
+
.prepare(`SELECT DISTINCT p.work_key FROM revision_work_pieces p
|
|
9
|
+
JOIN revision_work w ON w.id=p.work_id
|
|
10
|
+
JOIN revision_work_reservations r ON r.work_id=w.id
|
|
11
|
+
JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id
|
|
12
|
+
WHERE w.project_id=? AND w.kind=? AND w.state='failed' AND p.state!='done'
|
|
13
|
+
AND (SELECT a.outcome FROM attempts a WHERE a.work_piece_id=p.id ORDER BY a.rowid DESC LIMIT 1)='refusal'
|
|
14
|
+
ORDER BY p.work_key`)
|
|
15
|
+
.all(projectId, stage)
|
|
16
|
+
.map((row) => String(row.work_key));
|
|
17
|
+
}
|
|
18
|
+
export function requestSoftening(db, projectId, keys, at) {
|
|
19
|
+
for (const key of keys)
|
|
20
|
+
db.prepare("INSERT INTO prompt_softening(project_id,work_key,requested_at) VALUES (?,?,?) ON CONFLICT(project_id,work_key) DO UPDATE SET requested_at=excluded.requested_at").run(projectId, key, at);
|
|
21
|
+
}
|
|
22
|
+
export function softeningRequested(db, projectId, key) {
|
|
23
|
+
return (db
|
|
24
|
+
.prepare("SELECT 1 FROM prompt_softening WHERE project_id=? AND work_key=?")
|
|
25
|
+
.get(projectId, key) !== undefined);
|
|
26
|
+
}
|
|
27
|
+
export function clearSoftening(db, projectId, keys) {
|
|
28
|
+
for (const key of keys)
|
|
29
|
+
db.prepare("DELETE FROM prompt_softening WHERE project_id=? AND work_key=?").run(projectId, key);
|
|
30
|
+
}
|
|
31
|
+
export function softenMessages(prompt) {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
role: "user",
|
|
35
|
+
content: `An image generator's content filter refused the image prompt below. Rewrite it so it keeps the same scene, subject, composition and style but leaves out whatever a filter could flag: graphic violence, gore, nudity or sexual content, real people's names, weapons aimed at people, self-harm and hateful symbols. Describe such things indirectly or leave them out. Answer with the rewritten prompt only, no preamble and no quotes.
|
|
36
|
+
|
|
37
|
+
Prompt:
|
|
38
|
+
${prompt}`,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
// The model's answer, trimmed of the quotes and labels models add anyway.
|
|
43
|
+
export function softenedPrompt(answer) {
|
|
44
|
+
return answer
|
|
45
|
+
.trim()
|
|
46
|
+
.replace(/^(?:rewritten prompt|prompt)\s*:\s*/i, "")
|
|
47
|
+
.replace(/^["“](.*)["”]$/s, "$1")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
@@ -130,18 +130,25 @@ function existingWork(db, projectId, key, fingerprint) {
|
|
|
130
130
|
: { workId: z.string().parse(row.work_id), pieceId: z.string().parse(row.piece_id) };
|
|
131
131
|
}
|
|
132
132
|
function stageForKey(key) {
|
|
133
|
+
// A review runs in its item's stage; the narration's review in Audio.
|
|
134
|
+
if (key === "review:narration")
|
|
135
|
+
return "audio";
|
|
136
|
+
if (key.startsWith("review:shorts:"))
|
|
137
|
+
return "video";
|
|
138
|
+
if (key.startsWith("review:"))
|
|
139
|
+
return stageForKey(key.slice("review:".length));
|
|
133
140
|
const prefix = key.split(":")[0];
|
|
134
141
|
if (prefix === "research")
|
|
135
142
|
return "research";
|
|
136
|
-
if (prefix === "article" || prefix === "entry")
|
|
143
|
+
if (prefix === "article" || prefix === "entry" || prefix === "script")
|
|
137
144
|
return "article";
|
|
138
145
|
if (prefix === "audio" || prefix === "narration")
|
|
139
146
|
return "audio";
|
|
140
|
-
if (prefix === "image")
|
|
147
|
+
if (prefix === "image" || prefix === "reference")
|
|
141
148
|
return "images";
|
|
142
149
|
if (prefix === "thumbnail")
|
|
143
150
|
return "thumbnail";
|
|
144
|
-
if (prefix === "export" || prefix === "subtitles" || prefix === "video")
|
|
151
|
+
if (prefix === "export" || prefix === "subtitles" || prefix === "video" || prefix === "voices")
|
|
145
152
|
return "video";
|
|
146
153
|
if (prefix === "document")
|
|
147
154
|
return "document";
|
|
@@ -10,7 +10,7 @@ import { chaptersFrom, plannerMessages, subAgentMessages } from "./planner.js";
|
|
|
10
10
|
import { sourcedAnswer, synthesisMessages } from "./synthesis.js";
|
|
11
11
|
// A model that cannot ground on the web fails the stage immediately, with no fallback to
|
|
12
12
|
// what the model already knows.
|
|
13
|
-
export const webResearchUnsupported = "The chosen AI model can't search the web, which research needs. Choose a model with web search in Edit project → Providers, or turn research off, then
|
|
13
|
+
export const webResearchUnsupported = "The chosen AI model can't search the web, which research needs. Choose a model with web search in Edit project → Providers, or turn research off, then Try again.";
|
|
14
14
|
// What a chapter piece carries between runs: its title, and its notes once a sub-agent
|
|
15
15
|
// has answered. That is the whole of the resume.
|
|
16
16
|
const chapterPayload = z.object({ title: z.string(), notes: z.string().optional() });
|
|
@@ -25,7 +25,7 @@ export async function runResearch(deps, context, providers) {
|
|
|
25
25
|
if (choice === undefined || articlePrompt === undefined) {
|
|
26
26
|
// Admission refuses a run whose research is Generate without both, so
|
|
27
27
|
// reaching here is a bug in admission rather than something the user did.
|
|
28
|
-
throw new Error("No AI model or article prompt is set for research. Choose a model in Edit project → Providers and check Edit project → Prompts, then
|
|
28
|
+
throw new Error("No AI model or article prompt is set for research. Choose a model in Edit project → Providers and check Edit project → Prompts, then Try again.");
|
|
29
29
|
}
|
|
30
30
|
const brief = { articlePrompt, values: project.config.values };
|
|
31
31
|
try {
|
|
@@ -111,7 +111,7 @@ async function plan(deps, context, providers, choice, brief, add) {
|
|
|
111
111
|
previewLabel: "Planning research",
|
|
112
112
|
// An empty answer, or one with no chapter in it, is a failed attempt.
|
|
113
113
|
check: (given) => chaptersFrom(given.text).length === 0
|
|
114
|
-
? "The AI model didn't return any research topics.
|
|
114
|
+
? "The AI model didn't return any research topics. Try again; if it keeps happening, choose a different model in Edit project → Providers."
|
|
115
115
|
: undefined,
|
|
116
116
|
});
|
|
117
117
|
if (!answer.ok)
|
|
@@ -217,7 +217,7 @@ function section(label, messages) {
|
|
|
217
217
|
}
|
|
218
218
|
function payloadOf(piece) {
|
|
219
219
|
if (piece.payload === null) {
|
|
220
|
-
throw new Error("Slopify hit an internal error (a saved research topic is empty).
|
|
220
|
+
throw new Error("Slopify hit an internal error (a saved research topic is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
221
221
|
}
|
|
222
222
|
return chapterPayload.parse(JSON.parse(piece.payload));
|
|
223
223
|
}
|
|
@@ -41,9 +41,9 @@ export function endsWithSources(text) {
|
|
|
41
41
|
// but cannot be used is a failed attempt, so the wrapper retries it.
|
|
42
42
|
export function sourcedAnswer(who, text) {
|
|
43
43
|
if (text.trim() === "") {
|
|
44
|
-
return `The AI model returned an empty answer for ${who}.
|
|
44
|
+
return `The AI model returned an empty answer for ${who}. Try again; if it keeps happening, choose a different model in Edit project → Providers.`;
|
|
45
45
|
}
|
|
46
46
|
return endsWithSources(text)
|
|
47
47
|
? undefined
|
|
48
|
-
: `The AI model's answer for ${who} had no sources. Research needs web sources:
|
|
48
|
+
: `The AI model's answer for ${who} had no sources. Research needs web sources: Try again, or choose a model with web search in Edit project → Providers.`;
|
|
49
49
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Automatic reviews: a reviewer model looks at each finished item of a stage (the article,
|
|
2
|
+
// every image, the narration, the thumbnail, every short) before the run moves on, and says
|
|
3
|
+
// pass or fail with its reasons. A failed item is flagged, or made again while its retries
|
|
4
|
+
// last. Browser-safe: Play, Edit project and the project page read these names and limits too.
|
|
5
|
+
export const reviewStages = ["article", "images", "narration", "thumbnail", "shorts"];
|
|
6
|
+
// Off is the same as the stage having no entry, which is what every project saved before
|
|
7
|
+
// reviews existed has.
|
|
8
|
+
export const reviewModes = ["off", "flag", "redo"];
|
|
9
|
+
// ceiling: the owner's numbers. Two redos is what a person would try before looking at it
|
|
10
|
+
// themselves; five is already six paid images for one slot.
|
|
11
|
+
export const defaultReviewRetries = 2;
|
|
12
|
+
export const reviewRetriesMin = 0;
|
|
13
|
+
export const reviewRetriesMax = 5;
|
|
14
|
+
export const reviewStageLabels = {
|
|
15
|
+
article: "Article",
|
|
16
|
+
images: "Images",
|
|
17
|
+
narration: "Narration",
|
|
18
|
+
thumbnail: "Thumbnail",
|
|
19
|
+
shorts: "Shorts",
|
|
20
|
+
};
|
|
21
|
+
export const reviewModeLabels = {
|
|
22
|
+
off: "Off",
|
|
23
|
+
flag: "Flag only",
|
|
24
|
+
redo: "Flag and redo",
|
|
25
|
+
};
|
|
26
|
+
// The stages whose items are pictures, so the reviewer has to be shown them.
|
|
27
|
+
export const visionStages = ["images", "thumbnail", "shorts"];
|
|
28
|
+
// The text providers whose adapter hands the model image files (`capabilities.images` in
|
|
29
|
+
// `adapters/llm`): Claude Code reads them with its Read tool in a private folder, Codex
|
|
30
|
+
// attaches them with `--image`. The Gemini CLI and OpenRouter adapters send text only.
|
|
31
|
+
export const imageReviewers = ["claude-code", "codex"];
|
|
32
|
+
export function reviewModeOf(settings, stage) {
|
|
33
|
+
return settings?.stages[stage]?.mode ?? "off";
|
|
34
|
+
}
|
|
35
|
+
export function reviewRetriesOf(settings) {
|
|
36
|
+
return settings?.retries ?? defaultReviewRetries;
|
|
37
|
+
}
|
|
38
|
+
export function reviewedStages(settings) {
|
|
39
|
+
return reviewStages.filter((stage) => reviewModeOf(settings, stage) !== "off");
|
|
40
|
+
}
|
|
41
|
+
// The key a stage's picked Review prompt renders under in `config.rendered`.
|
|
42
|
+
export function reviewPromptKey(stage) {
|
|
43
|
+
return `review.${stage}`;
|
|
44
|
+
}
|
|
45
|
+
// A review's step is `review:` and the key of the item it reviews. The narration is
|
|
46
|
+
// reviewed as a whole, so its item key names no step.
|
|
47
|
+
export const narrationItemKey = "narration";
|
|
48
|
+
export function reviewKey(itemKey) {
|
|
49
|
+
return `review:${itemKey}`;
|
|
50
|
+
}
|
|
51
|
+
export function reviewedItemKey(key) {
|
|
52
|
+
return key.startsWith("review:") ? key.slice("review:".length) : undefined;
|
|
53
|
+
}
|
|
54
|
+
export function reviewSettingsForm(settings) {
|
|
55
|
+
return {
|
|
56
|
+
provider: settings.provider,
|
|
57
|
+
model: settings.model,
|
|
58
|
+
...(settings.thinking === undefined ? {} : { thinking: settings.thinking }),
|
|
59
|
+
retries: settings.retries === undefined ? "" : String(settings.retries),
|
|
60
|
+
stages: Object.fromEntries(reviewStages.flatMap((stage) => {
|
|
61
|
+
const picked = settings.stages[stage];
|
|
62
|
+
return picked === undefined
|
|
63
|
+
? []
|
|
64
|
+
: [[stage, { mode: picked.mode, prompt: picked.prompt ?? "" }]];
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// The form back to settings: only the reviews of stages that make something (`makes`), and
|
|
69
|
+
// none left is no reviews at all, which is what every project saved before them has. Retries
|
|
70
|
+
// that are not a number are left out and reported, so the form marks the field.
|
|
71
|
+
export function reviewSettingsFromForm(form, makes) {
|
|
72
|
+
const stages = Object.fromEntries(reviewStages.flatMap((stage) => {
|
|
73
|
+
const picked = form.stages[stage];
|
|
74
|
+
if (picked === undefined || picked.mode === "off" || !makes(stage))
|
|
75
|
+
return [];
|
|
76
|
+
return [
|
|
77
|
+
[stage, { mode: picked.mode, ...(picked.prompt.trim() ? { prompt: picked.prompt } : {}) }],
|
|
78
|
+
];
|
|
79
|
+
}));
|
|
80
|
+
if (Object.keys(stages).length === 0)
|
|
81
|
+
return { settings: undefined };
|
|
82
|
+
const retries = form.retries.trim() === "" ? undefined : Number(form.retries);
|
|
83
|
+
const valid = retries === undefined || Number.isFinite(retries);
|
|
84
|
+
return {
|
|
85
|
+
settings: {
|
|
86
|
+
provider: form.provider,
|
|
87
|
+
model: form.model,
|
|
88
|
+
...(form.thinking === undefined ? {} : { thinking: form.thinking }),
|
|
89
|
+
...(retries === undefined || !valid ? {} : { retries }),
|
|
90
|
+
stages,
|
|
91
|
+
},
|
|
92
|
+
...(valid
|
|
93
|
+
? {}
|
|
94
|
+
: {
|
|
95
|
+
retriesProblem: `Enter a number of redos between ${String(reviewRetriesMin)} and ${String(reviewRetriesMax)} in the Reviews section.`,
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export const defaultReviewPrompts = {
|
|
100
|
+
article: "Review this article against the article prompt it was written from. Fail it when it breaks the prompt's rules on length, structure or tone; when it states names, places, events or facts that neither the prompt nor the research notes support (invented lore); or when a section repeats another section's content. Small style preferences are not a reason to fail it.",
|
|
101
|
+
images: "Review this image against the brief it was drawn from. Fail it when a hand or body is malformed (extra or missing fingers, fused limbs, broken anatomy); when it shows stray text, letters, captions or watermarks the brief did not ask for; when it does not show what the brief asks for; or, when an establishing image is attached, when it does not match it (a different character, palette or style).",
|
|
102
|
+
narration: "Review this narration against the text it was read from. The transcript was heard back from the audio; the gaps list stretches of text the timing could not hear. Fail it when words, sentences or passages are missing or garbled in a way a listener would notice. Ignore punctuation, numbers written out, and small differences in how words were transcribed.",
|
|
103
|
+
thumbnail: "Review this thumbnail as it would be seen on a phone, about 320 pixels wide. Fail it when its text (if any) cannot be read at that size, when the subject is unclear or cluttered, when a hand or body is malformed, or when it shows stray letters or watermarks.",
|
|
104
|
+
shorts: "Review these images from one vertical short, as they would be seen full screen on a phone. Fail them when the subject is unclear, when a hand or body is malformed, when they show stray text, letters or watermarks, or when they do not fit the short's title.",
|
|
105
|
+
};
|