Skip to content

For developers

Dev docs

Every model, setting and config AdLoop uses to make content — in the code and on the GPU factory. Rendered from docs/DEV_DOCS.md in the repo.
On this page
  1. 1. Overview
  2. 2. Text: LLM copywriter
  3. 3. Pictures: SDXL storyboard (in product)
  4. 4. Video: scene clips with Wan 2.1 I2V (prototype)
  5. 5. Talking avatar: InfiniteTalk (rejected)
  6. 6. Voice-over (prototype)
  7. 7. Captions (prototype)
  8. 8. Final edit (prototype)
  9. 9. GPU server ("GPU factory")
  10. 10. GPU PC setup (WSL2)
  11. 11. Licences
  12. 12. AdLoop configuration
  13. 13. Timings (RTX 5060 Ti, 12 GB WSL RAM)
  14. 14. Decisions log

Every model, setting and config used to make AdLoop's content, on the code side and on the GPU PC ("the GPU factory"). Values are the ones in the code as of 19 Oct 2026. The live panel at the top of the Dev docs page shows what is running right now.

Status labels used below:

  • In product: wired into AdLoop and used from the UI.
  • Prototype: a working script in gpu_server/prototypes/, used for the first test ad, not yet wired into the UI.
  • Rejected / not used: tried and dropped, kept here so nobody repeats it.

1. Overview

Mac or PC browser ─► Website (Next.js, :3000) ─► AdLoop backend (FastAPI, :8000)
                                                   │  LLM: qwen3-30b (scripts, briefs, storyboards)
                                                   └► GPU server (FastAPI, :8500) ─► ComfyUI (:8188)
                                                        one GPU job at a time        SDXL, Wan 2.1, InfiniteTalk
ContentModel / toolStatus
Creative brief, scripts, round insightsqwen3-30b (LLM)In product
Storyboard: shots, cast and lookqwen3-30b (LLM)In product
Storyboard picturesSDXL: Juggernaut XL v9 via ComfyUIIn product
Placeholder video and pictures (no AI)ffmpeg + libassIn product
AI presenter facesSDXL: Juggernaut XL v9Prototype
Scene clips (picture → 3 s video)Wan 2.1 I2V 14B (GGUF Q4) + lightx2v LoRAPrototype
Word timings for captionstorchaudio MMS forced alignmentPrototype (licence: non-commercial, see §11)
Word-by-word captionsASS subtitles rendered by libassPrototype
Voice-overOwner's own recording + ffmpeg masteringPrototype
Final editffmpegPrototype
Talking avatarInfiniteTalk (Wan 2.1 based)Rejected
AI voice (TTS)svara-TTS v1, IndicF5, Indic Parler-TTSNot used yet

2. Text: LLM copywriter

SettingValue
Modelqwen3-30b (Qwen3 30B, Apache-2.0), through the OpenAI-compatible gateway https://llm.enigmahealth.io/v1
Runtime providerollama provider of agent-runtime (OpenAI-compatible client)
Max output tokens8000: AGENT_RUNTIME_LLM_OLLAMA_MAX_OUTPUT_TOKENS (the gateway's default of 512 cuts scripts off)
Timeout180 s: AGENT_RUNTIME_LLM_OLLAMA_TIMEOUT_SECONDS
Temperature / top-pNot set. The gateway's defaults apply.
Output formatStructured output (Pydantic schemas) via LLMService.complete_structured
ChooserAD_ENGINE_COPYWRITER=auto: LLM when a provider is configured, otherwise TemplateCopywriter (offline). Tests always pin template.
Codesrc/ad_engine/copywriter.py

What it writes:

TaskSchemaNotes
Brand briefBriefOutOptional web research (Tavily) when AGENT_RUNTIME_TAVILY_API_KEY is set. Not set today.
ScriptsScriptBatch of ScriptDraftWritten 4 per call (_BATCH), so Indian-language scripts don't overrun the output limit. Each later batch is told what was already written. Round 2+: ~70% are variations of winners.
Round insightInsightOutPlain-English takeaways from winners and losers
StoryboardStoryboardDraft = cast + look + scenes[] (line, image_prompt, motion_prompt)About one scene per 3.5 s of speech: scene_count = clamp(round(duration / 3.5), 3, 8)
Scene redoSceneDraftGets the cast, the look, the other shots and the last 5 feedback notes. The spoken line is never changed.

Rules in the prompts:

  • Use only facts the business gave.
  • Never invent prices, distances, amenities, awards or counts.
  • Never say "guaranteed", "100%", "cure" or "risk-free".
  • Always end with a call to action.
  • Storyboards are one short film: the same cast, place and look in every shot, with people mostly in wide or medium shots, not posing. Pictures contain no text or logos.

Checks after the LLM:

  • Compliance (compliance.py) flags risky claims and numbers that aren't in the business details. For example, "24/7 security" was flagged as "7, 24".
  • Scene lines must add up to the script word for word, compared on letters and digits only, because the voice-over is cut by them. On a mismatch the writer gets one retry with the problem spelled out. If it still fails, the script is split at sentence ends and the model's pictures are kept by position (studio._write_scenes).

The creative brief in prompts (src/ad_engine/creative.py): every chosen option becomes an English phrase, e.g. modern_casual → "modern smart-casual clothing". Options left on "AI decides" are described as "your choice". When clothing is on AI decides, the prompt adds "do not default to traditional dress". Unknown answers are dropped (normalise). The question list is mirrored in frontend/lib/creative.ts, and tests/test_studio.py fails if the two drift.

Known quality issues (Telugu, qwen3-30b):

  • Draft-grade language: stutters ("nenu nenu nenu"), Hindi words mixed in ("ho jayegi"), spelling slips.
  • Invented non-numeric details.
  • A stronger model plus a native-speaker review are needed before client work.

3. Pictures: SDXL storyboard (in product)

SettingValue
ModelJuggernaut XL v9 (RunDiffusion/Juggernaut-XL-v9, file Juggernaut-XL_v9_RunDiffusionPhoto_v2.safetensors, 7.11 GB), saved as ComfyUI/models/checkpoints/Juggernaut-XL_v9.safetensors
LicenceCreativeML OpenRAIL-M (commercial use allowed, with use restrictions)
Runs inComfyUI, called by the GPU server (gpu_server/src/gpu_server/images.py, ComfyImageRenderer)
Size768 × 1344 (9:16-ish, SDXL-native)
Steps / CFG30 / 4.5
Sampler / schedulerdpmpp_2m / karras
SeedStable per scene and revision: the first 4 bytes of sha256("{scene_id}:{revisions}"). A redo gives a new picture; a retry gives the same one.
Time~12 s per picture (~25 s for the first one, while SDXL loads) on the RTX 5060 Ti
ComfyUI wait limit600 s per picture

Prompt = scene image_prompt + variant look + this fixed style:

cinematic film still, shot on 35mm, natural colour grading, shallow depth of field, photorealistic, highly detailed, vertical composition

Negative prompt:

cartoon, illustration, 3d render, anime, painting, deformed, blurry, text, letters, watermark, logo, signage, subtitles, distorted faces, extra limbs, extra fingers, low quality, oversaturated

Consistency across scenes:

  • The LLM writes one cast (each character's age, hair and exact clothing) and one look (lighting and colour grade).
  • The cast is copied into every scene that shows people.
  • Known gap: wardrobe still drifts between shots (e.g. orange instead of beige). This needs reference-image conditioning (IP-Adapter / face ID), planned as Phase C.

Without ComfyUI (GPU_SERVER_COMFY_URL unset), /v1/images returns a flat placeholder PNG (ffmpeg color + hue from the seed). Without a GPU server at all, AdLoop draws an SVG card with the line and the picture description (StoryboardBackend.generate_image).

Faces (prototype, make_faces.py): same model, 832 × 1216, 30 steps, CFG 4.5, dpmpp_2m karras, seeds 11/22/33/44. Prompt: "professional head and shoulders portrait photo of a friendly South Indian man, early 30s … photorealistic, 85mm lens". Face #3 (seed 33) was picked.


4. Video: scene clips with Wan 2.1 I2V (prototype)

Turns one approved scene picture into a ~3 s clip (gpu_server/prototypes/scenes.py, animate).

PartFile (in ComfyUI/models/)SizeSource
Video modeldiffusion_models/WanVideo/wan2.1-i2v-14b-480p-Q4_K_M.gguf11.34 GBcity96/Wan2.1-I2V-14B-480P-gguf (Wan 2.1, Apache-2.0)
Speed LoRAloras/WanVideo/Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank64_bf16.safetensors0.74 GBKijai/WanVideo_comfy
Text encodertext_encoders/umt5-xxl-enc-fp8_e4m3fn.safetensors6.73 GBKijai/WanVideo_comfy
Image encoderclip_vision/clip_vision_h.safetensors1.26 GBComfy-Org/Wan_2.1_ComfyUI_repackaged
VAEvae/wanvideo/Wan2_1_VAE_bf16.safetensors0.25 GBKijai/WanVideo_comfy
SettingValue
Size / frames / fps480 × 832, 81 frames, 25 fps (3.24 s)
Steps / CFG / shift4 / 1.0 / 5.0 (lightx2v strength 1.0)
Scheduler / seeddpm++_sde / 42
Image-to-video encodenoise_aug_strength 0.03, start/end latent strength 1.0, fun_or_fl2v_model false
Model loaderbase_precision fp16_fast, quantization disabled (already GGUF), load_device offload_device, attention sdpa
Block swapblocks_to_swap 15, offload_img_emb and offload_txt_emb true, use_non_blocking false, prefetch_blocks 0
Text encodeWanVideoTextEncodeCached, quantization fp8_e4m3fn, disk cache on, device gpu
Motion promptThe scene's motion_prompt. Negative: "bright tones, overexposed, static, blurred details, subtitles, text, worst quality, … walking backwards, morphing faces"
OutputVHS_VideoCombine h264 mp4, CRF 17
Time~3.5–5 min per clip; ~23 min for a 20 s ad (5 scenes)

5. Talking avatar: InfiniteTalk (rejected)

Worked technically. Vijay rejected it because the lip sync was off and the face looked AI-made. Kept in prototypes/talk.py / run_talk.sh.

SettingValue
ModelsInfiniteTalk Wan2_1-InfiniteTalk_Single_Q4_K_M.gguf (1.40 GB, Kijai/WanVideo_comfy_GGUF, Apache-2.0) on the same Wan 2.1 14B Q4 base, plus wav2vec2-chinese-base_fp16.safetensors (0.19 GB) for audio
Size / window480 × 832, frame_window_size 81, motion_frame 9, mode infinitetalk, colormatch disabled
Sampler6 steps, CFG 1.0, shift 11.0, dpm++_sde, seed 2, lightx2v LoRA
Audio embedsnormalize_loudness true, fps 25, audio_scale 1, audio_cfg_scale 1, multi_audio_type para. MelBandRoFormer vocal separation removed (clean recording).
Time / memory~4 min sampling per 3.2 s window (5.6 min total); peak 12.4 GB VRAM allocated, 11.4 GB RAM

6. Voice-over (prototype)

Source: Vijay's own phone recording, used with his consent. Measured at 19 s, mono, 16 kHz AAC, peaking at +0.3 dBFS (clipped). For better results, record at 20–30 cm, as WAV / 48 kHz.

Mastering chain (prototypes/master_voice.sh, ffmpeg only, applied in this order):

StepFilter
Rumble cuthighpass=f=85
Noise reductionafftdn=nr=12:nf=-45:tn=1
Chest / authorityequalizer=f=110:t=q:w=0.8:g=2.5
Mud cutequalizer=f=220:t=q:w=1.0:g=-2.5
Presence / clarityequalizer=f=3200:t=q:w=1.2:g=3
Airequalizer=f=6000:t=q:w=1.5:g=1.5
De-essdeesser=i=0.35
Compressionacompressor=threshold=-22dB:ratio=3.5:attack=8:release=120:makeup=5:knee=4
Resamplearesample=48000:resampler=soxr
Loudnessloudnorm=I=-15:TP=-1.5:LRA=6, then alimiter=limit=0.89

Result: -20.2 → -16.5 LUFS integrated, loudness range 3.8 → 1.5 LU. The final ad mix applies loudnorm=I=-14:TP=-1.5 (social-media target).

AI voice (not used yet; free tools only):

ModelLicenceNote
svara-TTS v1 (kenpath/svara-tts-v1)Apache-2.0Llama-3.2-3B (Orpheus-style) + SNAC 24 kHz decoder, 19 Indian languages, "Telugu (Male/Female)" voices, zero-shot cloning. The download was stopped (slow internet). Planned settings: temperature 0.6, top-p 0.9, repetition penalty 1.1, max 1600 new tokens per line.
IndicF5 (ai4bharat/IndicF5)MIT, gatedRanked #1 for Telugu in a public listening benchmark. Clones from a reference clip and its transcript. 24 kHz.
Indic Parler-TTS (ai4bharat/indic-parler-tts)Apache-2.0, gatedThe voice is described in words. Its "Command" emotion isn't supported for Telugu.
ElevenLabs, Sarvam BulbulPaidNot allowed until Vijay approves paid APIs

7. Captions (prototype)

Word timings (prototypes/align.py):

  • torchaudio MMS_FA forced alignment (ctc_alignment_mling_uroman, 1.18 GB, with_star=False) aligns the known script to the audio.
  • The Telugu text is romanised with uroman 1.3.1.1.
  • Tokens the aligner can't read get a spoken form, e.g. 2BHK → "tu bi ech ke".
  • A word's start is clamped to at most 0.6 s before its end, because the aligner can absorb leading silence.
  • Confidence on the test ad was 0.6–1.0 per word.

Rendering (prototypes/captions.py): an ASS subtitle file burned in by libass (HarfBuzz shaping) at 1080 × 1920.

SettingValue
FontsHind Guntur Bold (Telugu) and Poppins ExtraBold/Bold (Latin), both SIL OFL, installed in ~/.local/share/fonts/adloop by fonts.sh. Noto Sans (all scripts) from fonts-noto-core is the fallback.
Hind Guntur quirkIt has very tall line metrics, so libass draws it at ~55% of the nominal size. Sizes are set high to compensate.
PhrasesAt most 3 words or 14 characters. A new phrase starts after punctuation or a pause over 0.35 s. A phrase ends when the next one starts (check_overlap.py verifies this).
PositionPhrase centre at y = 1330, inside the Reels/Shorts safe zone (the bottom ~350 px and right ~140 px are covered by app buttons).
Pop (chosen by Vijay)Size 128, outline 7, shadow 4. The spoken word turns the highlight colour and scales to 112%.
HighlightSize 124, outline 4. The spoken word gets a thick coloured outline as a marker (border 22, blur 1).
CleanSize 112, no outline, shadow 3. Words appear as they are spoken; earlier words fade slightly.
EntranceScale 80% → 106% → 100% over 170 ms (Clean: rise 18 px and fade in, 120–160 ms)
Highlight colour#FFD400 by default (user option; presets #22C55E, #7C3AED, #2563EB, #F43F5E, #FFFFFF, or custom)
Keyword callout"2BHK" in Poppins, size 190, at y = 560, with a bounce (0 → 118% → 100%), plus "NEAR METRO" (size 76) in the highlight colour
CTA end cardGreen pill 860 × 180 (#0BD15E) at y = 1000, "ఇప్పుడే కాల్ చేయండి" at size 124, and the phone number (Poppins 84) 0.6 s later. Phrase captions stop when the CTA appears.
AI label"AI ద్వారా రూపొందించబడింది" in a semi-transparent pill 470 × 70 at the top-left (40, 150), on screen for the whole ad. Required by India's IT Rules, Feb 2026.

Any language (prototypes/captions_v2.py): the same engine, driven by a small JSON config with keys style, colour, font, latin_font, total, cta_at, cta, phone, cta_sub, label and callouts[]. For example, Devanagari uses the Hind font (SIL OFL, installed by font_hi.sh); Telugu uses Hind Guntur. Hind has the same tall-metrics quirk, so the same sizes apply.

Silent videos (no voice-over):

  • Each scene gets a fixed slot (3.4 s).
  • Its words are spread over the slot by length: the window starts 0.25 s in and ends 0.35 s before the slot ends.
  • The Pop highlight still moves word by word.
  • A silent stereo track (anullsrc, AAC 128k) is added so social apps accept the upload.
  • Example: the Shiv Ashray Hindi ad (prototypes/hotel_*.py), 6 scenes plus a 3 s end card = 23.4 s.

8. Final edit (prototype)

prototypes/compose.sh and render_caps.sh, ffmpeg 8.0.1:

StepSetting
Scene timingEach clip is placed on its sentence, using the pauses in the voice track (silencedetect=n=-35dB:d=0.25). Test ad: 0–3.6, 3.6–7.45, 7.45–11.15, 11.15–14.4 and 14.4–17.0 s, then a 2.6 s end card (19.6 s total).
Stretchsetpts=max(1, slot/clip)·PTS: clips are slowed slightly to fill their slot, never sped up
Upscalescale=1080:1920:flags=lanczos, fps=30 (generated at 480 × 832)
End cardLast frame of the final scene, eq=brightness=-0.25:saturation=0.8, gblur=sigma=6
Captionsass=caps.ass:fontsdir=~/.local/share/fonts/adloop
Audioaresample=48000, apad, atrim, loudnorm=I=-14:TP=-1.5, AAC 160 kb/s
Videolibx264, preset medium, CRF 18, yuv420p, +faststart

Placeholder renderer (in product, gpu_server/renderers.py):

  • Used for /v1/render, with no AI involved.
  • 1080 × 1920, 30 fps, 5–60 s.
  • Shows the headline, on-screen points, CTA, avatar image and AI label, rendered with libass and Noto fonts.
  • Audio is silent (anullsrc).
  • Encoding: x264 veryfast, AAC 128k.

9. GPU server ("GPU factory")

gpu_server/: FastAPI with one worker thread, so one GPU job at a time. 16 GB of VRAM can't hold an image model and a video model together.

EndpointPurpose
GET /healthDevice, video renderer, image renderer, models, queue length
POST /v1/render (multipart: spec, voice_sample, avatar_image)Video job → {"job_id"}
POST /v1/images (JSON: prompt, negative_prompt, width, height, seed)Storyboard picture job → {"job_id"}
GET /v1/jobs/{id}{"status": queued/running/done/failed, "error", "kind"}
GET /v1/jobs/{id}/video, /imageThe finished file
  • Job folders: data/jobs/<id>/ holds job.json, the inputs and output.mp4 / output.png. A task's result is renamed to output.* only after it finishes (an atomic rename), so a crash never leaves a half-written file looking done. After a restart, status is rebuilt from the folders.
  • Auth: Authorization: Bearer <GPU_SERVER_TOKEN> on every request, when set.
GPU server envDefaultMeaning
GPU_SERVER_COMFY_URLemptyComfyUI for real pictures (http://127.0.0.1:8188). Empty means placeholder pictures.
GPU_SERVER_SDXL_CHECKPOINTJuggernaut-XL_v9.safetensorsCheckpoint name in ComfyUI
GPU_SERVER_TOKENemptyShared password. Set it before exposing the port beyond the PC.
GPU_SERVER_DATA_DIRdataJob folders
GPU_SERVER_LOG_LEVELINFO

AdLoop's client (media.GpuServerBackend) polls every 3 s and gives up after AD_ENGINE_GPU_TIMEOUT_SECONDS (default 1800).


10. GPU PC setup (WSL2)

ItemValue
HardwareNVIDIA RTX 5060 Ti 16 GB (driver 595.79), 16 GB system RAM
WSLUbuntu on WSL2. C:\Users\VIJAY\.wslconfig: memory=12GB, swap=16GB
ComfyUI~/work/ComfyUI (commit 8f04fd6), own uv venv on Python 3.12 (Ubuntu's python3 is 3.14)
Custom nodesComfyUI-WanVideoWrapper 088128b, ComfyUI-VideoHelperSuite 4d907be, ComfyUI-KJNodes 17195fc
PyTorchtorch 2.11.0+cu128, torchaudio 2.11.0, torchvision 0.26.0, from the cu128 index (RTX 50-series = sm_120)
Pinnedtransformers 4.57.6 (5.x breaks the InfiniteTalk audio encoder), diffusers 0.35.2, huggingface_hub 0.36.2 (newer diffusers needs hub ≥ 1.0, which transformers 4.x forbids), gguf 0.19.0, accelerate 1.15.0, uroman 1.3.1.1, snac 1.2.1
Otherffmpeg 8.0.1, Node 20.20.2, fonts-noto-core
Start everythingbash ~/work/adloop/scripts/start_pc.sh starts ComfyUI, the GPU server with ComfyUI connected, the backend (auto-reload) and the website, skipping what's already running. Logs go to ~/work/logs/.
Ports3000 website, 8000 backend, 8500 GPU server, 8188 ComfyUI

Gotchas (each one cost a failed run):

  • Strip the torch* lines from custom-node requirements.txt files, or they replace the cu128 build.
  • Under WSL, pinned host memory is limited. Block swap needs use_non_blocking=false, or it fails with CUDA error: out of memory even when VRAM is free.
  • blocks_to_swap: 20 with pinned memory runs out of memory; 40 fills the 12 GB of RAM and thrashes swap (GPU at 2%); 15 works.
  • Use sdpa attention. Don't try to build flash-attn or sageattention for sm_120 under WSL.
  • torchaudio.load needs torchcodec, so load audio through ffmpeg instead.
  • Background jobs must be started with setsid nohup (prototypes/bg.sh). wsl -- bash -c "cmd &" dies with the call. Keep one Ubuntu window open, or Windows shuts WSL down.
  • A stalled Hugging Face download can leave a process holding the file lock. Kill it before retrying.

11. Licences

ItemLicenceCommercial use
qwen3-30b (Qwen3)Apache-2.0Yes
Juggernaut XL v9CreativeML OpenRAIL-MYes, with use restrictions
Wan 2.1 I2V 14B, InfiniteTalkApache-2.0Yes
MMS forced aligner (torchaudio MMS_FA)CC-BY-NC 4.0No. Replace before client work (e.g. alignment from a Whisper- or wav2vec2-based model with a commercial licence)
lightx2v LoRA, umt5-xxl, CLIP vision H, wav2vec2-chinese-base, uromanNot yet verifiedCheck before client work
Hind Guntur, PoppinsSIL OFL 1.1Yes
react-markdown, remark-gfm (docs page)MITYes
MusicGen (not used)Weights CC-BY-NCNo, so don't use it for music

12. AdLoop configuration

AdLoop's .env (never committed):

VariableUsed for
AGENT_RUNTIME_DEFAULT_LLM_PROVIDERollama (OpenAI-compatible gateway)
AGENT_RUNTIME_LLM_OLLAMA_BASE_URL / _API_KEYGateway address and key
AGENT_RUNTIME_LLM_OLLAMA_TIMEOUT_SECONDS180
AGENT_RUNTIME_LLM_OLLAMA_MAX_OUTPUT_TOKENS8000
AD_ENGINE_COPY_MODELqwen3-30b
AD_ENGINE_COPYWRITERauto (llm / template)
AD_ENGINE_DATABASE_URLsqlite:///data/ad_engine.db
AD_ENGINE_MEDIA_DIRdata/media (storyboards in data/media/storyboards/<campaign>/<ad>/)
AD_ENGINE_GPU_SERVER_URL / _TOKEN / _TIMEOUT_SECONDSThe GPU factory. URL http://127.0.0.1:8500 on the PC; timeout 1800 s
AGENT_RUNTIME_CORS_ALLOW_ORIGINS, AGENT_RUNTIME_LOG_LEVELServer

Production limits (src/ad_engine/studio.py):

  • At most 3 scripts per round become videos (MAX_IN_PRODUCTION).
  • Redo prompts use the last 5 feedback notes for that scene or storyboard.
  • Scene and storyboard changes go through a lock and re-read the ad first, so a click during a slow LLM or GPU call is never lost.

13. Timings (RTX 5060 Ti, 12 GB WSL RAM)

StepTime
Brand brief (LLM)~9 s
3 scripts (LLM)~24 s
Storyboard, 4 scenes (LLM + 4 SDXL pictures)~66 s
Redo one scene with a note~20 s
SDXL picture~12 s (first ~25 s)
Wan 2.1 clip, 3.24 s~3.5–5 min
InfiniteTalk, 3.2 s~5.6 min
Full 20 s short-film ad (5 scenes + edit)~23 min

14. Decisions log

DateDecision
19 OctTelugu + Hyderabad first; sell results (leads) as a service before selling software
19 OctTalking-head avatar rejected. Video ads are scene-based short films + voice-over.
19 OctUp to 2–3 picked scripts per round become videos; the storyboard is approved before any animation
19 OctFree/local models only until Vijay approves paid APIs
19 OctReal faces allowed only with a signed consent form per person; AI faces by default
19 OctCaptions: Pop + yellow chosen, as a user option. The brief lives behind a Preferences drawer.
19 OctVoice-over from the owner's own recording for now (the AI voice download was stopped)
19 OctSecond ad: Shiv Ashray, Varanasi (Hindi), made silent and captions-led. The internet was too slow for the 13 GB svara-TTS model. The free Piper Hindi voices were also ruled out: pratham and priyamvada are CC-BY-NC-SA, and rohan uses an IIT Madras licence that hasn't been checked. The AI's Hinglish scripts were too long for captions, so the script was rewritten by hand into 6 short Devanagari lines using only facts from the hotel's website. Scenes 2, 5 and 6 were redone (garbled sign text, wrong-looking people).