KPrashanth commited on
Commit
85ba912
·
verified ·
1 Parent(s): 58c4889

Upload 47 files

Browse files
Files changed (47) hide show
  1. .gitignore +207 -0
  2. LICENSE +21 -0
  3. Marketeer_Patched_Video.ipynb +689 -0
  4. README.md +1 -12
  5. app.py +13 -0
  6. blueprint.md +340 -0
  7. check.ipynb +70 -0
  8. core_logic/__init__.py +0 -0
  9. core_logic/__pycache__/__init__.cpython-310.pyc +0 -0
  10. core_logic/__pycache__/chat_agent.cpython-310.pyc +0 -0
  11. core_logic/__pycache__/chat_chain.cpython-310.pyc +0 -0
  12. core_logic/__pycache__/copy_pipeline.cpython-310.pyc +0 -0
  13. core_logic/__pycache__/langchain_llm.cpython-310.pyc +0 -0
  14. core_logic/__pycache__/llm_client.cpython-310.pyc +0 -0
  15. core_logic/__pycache__/llm_config.cpython-310.pyc +0 -0
  16. core_logic/__pycache__/rewrite_tools.cpython-310.pyc +0 -0
  17. core_logic/__pycache__/video_pipeline.cpython-310.pyc +0 -0
  18. core_logic/__pycache__/video_schema.cpython-310.pyc +0 -0
  19. core_logic/chat_agent.py +216 -0
  20. core_logic/chat_chain.py +170 -0
  21. core_logic/copy_pipeline.py +114 -0
  22. core_logic/langchain_llm.py +45 -0
  23. core_logic/llm_client.py +120 -0
  24. core_logic/llm_config.py +50 -0
  25. core_logic/rewrite_tools.py +40 -0
  26. core_logic/video_pipeline.py +271 -0
  27. core_logic/video_schema.py +94 -0
  28. helpers/__init__.py +0 -0
  29. helpers/__pycache__/__init__.cpython-310.pyc +0 -0
  30. helpers/__pycache__/blueprints.cpython-310.pyc +0 -0
  31. helpers/__pycache__/json_utils.cpython-310.pyc +0 -0
  32. helpers/__pycache__/platform_rules.cpython-310.pyc +0 -0
  33. helpers/__pycache__/platform_styles.cpython-310.pyc +0 -0
  34. helpers/__pycache__/validators.cpython-310.pyc +0 -0
  35. helpers/blueprints.py +161 -0
  36. helpers/json_utils.py +72 -0
  37. helpers/platform_rules.py +149 -0
  38. helpers/platform_styles.py +111 -0
  39. helpers/validators.py +88 -0
  40. requirements.txt +17 -0
  41. test_llm_backend.py +29 -0
  42. todo.md +3 -0
  43. ui/__init__.py +0 -0
  44. ui/__pycache__/__init__.cpython-310.pyc +0 -0
  45. ui/__pycache__/gradio_ui.cpython-310.pyc +0 -0
  46. ui/gradio_ui.py +583 -0
  47. ui/gradio_ui_1.py +323 -0
.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Katakam Prashanth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Marketeer_Patched_Video.ipynb ADDED
@@ -0,0 +1,689 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# 🧠 Marketeer — Conversational Marketing Bot (Patched)\n",
8
+ "\n",
9
+ "**This version fixes video scripting returning empty content** by:\n",
10
+ "- Using Gemma-friendly prompting (no `system` role for JSON blocks)\n",
11
+ "- Strong JSON extraction + graceful fallback per beat\n",
12
+ "- REPL `/video` is fully wired to `make_video()`\n",
13
+ "- Includes a quick self‑test cell\n"
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "execution_count": 12,
19
+ "metadata": {},
20
+ "outputs": [
21
+ {
22
+ "name": "stdout",
23
+ "output_type": "stream",
24
+ "text": [
25
+ "▶ Python: 3.10.11\n",
26
+ "▶ Platform: Linux-6.6.87.2-microsoft-standard-WSL2-x86_64-with-glibc2.39\n",
27
+ "\n",
28
+ "▶ nvidia-smi:\n",
29
+ "Thu Nov 13 00:51:03 2025 \n",
30
+ "+-----------------------------------------------------------------------------------------+\n",
31
+ "| NVIDIA-SMI 560.35.03 Driver Version: 561.09 CUDA Version: 12.6 |\n",
32
+ "|-----------------------------------------+------------------------+----------------------+\n",
33
+ "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n",
34
+ "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n",
35
+ "| | | MIG M. |\n",
36
+ "|=========================================+========================+======================|\n",
37
+ "| 0 NVIDIA GeForce RTX 3060 ... On | 00000000:01:00.0 On | N/A |\n",
38
+ "| N/A 57C P8 13W / 85W | 4961MiB / 6144MiB | 13% Default |\n",
39
+ "| | | N/A |\n",
40
+ "+-----------------------------------------+------------------------+----------------------+\n",
41
+ " \n",
42
+ "+-----------------------------------------------------------------------------------------+\n",
43
+ "| Processes: |\n",
44
+ "| GPU GI CI PID Type Process name GPU Memory |\n",
45
+ "| ID ID Usage |\n",
46
+ "|=========================================================================================|\n",
47
+ "| 0 N/A N/A 6286 C /python3.10 N/A |\n",
48
+ "+-----------------------------------------------------------------------------------------+\n",
49
+ "\n"
50
+ ]
51
+ }
52
+ ],
53
+ "source": [
54
+ "import sys, subprocess, platform\n",
55
+ "print(f'▶ Python: {sys.version.split()[0]}')\n",
56
+ "print(f'▶ Platform: {platform.platform()}')\n",
57
+ "print('\\n▶ nvidia-smi:')\n",
58
+ "try:\n",
59
+ " print(subprocess.check_output(['nvidia-smi'], text=True))\n",
60
+ "except Exception:\n",
61
+ " print('(no GPU visible)')\n"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "markdown",
66
+ "metadata": {},
67
+ "source": [
68
+ "## Install base libs\n",
69
+ "(If already installed, this is a no-op.)"
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "code",
74
+ "execution_count": null,
75
+ "metadata": {},
76
+ "outputs": [],
77
+ "source": [
78
+ "# %pip -q install --upgrade pip\n",
79
+ "# %pip -q install transformers accelerate sentence-transformers faiss-cpu pypdf textstat regex tiktoken rich"
80
+ ]
81
+ },
82
+ {
83
+ "cell_type": "code",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "import torch, transformers, textwrap, re, json\n",
89
+ "from transformers import AutoTokenizer, AutoModelForCausalLM\n",
90
+ "from typing import List, Dict, Any\n",
91
+ "from collections import deque\n",
92
+ "print({'torch': torch.__version__, 'transformers': transformers.__version__})"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": null,
98
+ "metadata": {},
99
+ "outputs": [],
100
+ "source": [
101
+ "from typing import Dict, List\n",
102
+ "import re\n",
103
+ "\n",
104
+ "PLATFORM_RULES: Dict[str, Dict[str, int]] = {\n",
105
+ " 'Instagram': {'cap': 2200, 'hashtags_max': 5, 'emoji_max': 5},\n",
106
+ " 'Facebook': {'cap': 125, 'hashtags_max': 0, 'emoji_max': 1},\n",
107
+ " 'LinkedIn': {'cap': 3000, 'hashtags_max': 3, 'emoji_max': 2},\n",
108
+ " 'Google Ads': {'cap': 90, 'hashtags_max': 0, 'emoji_max': 0},\n",
109
+ " 'Twitter/X': {'cap': 280, 'hashtags_max': 2, 'emoji_max': 2},\n",
110
+ "}\n",
111
+ "\n",
112
+ "BANNED_MAP = {\n",
113
+ " r'\\bguarantee(d|s)?\\b': 'aim to',\n",
114
+ " r'\\bno[-\\s]?risk\\b': 'low risk',\n",
115
+ " r'\\bno[-\\s]?questions[-\\s]?asked\\b': 'hassle-free',\n",
116
+ " r'\\b#?1\\b': 'top-rated',\n",
117
+ " r'\\bbest\\b': 'trusted',\n",
118
+ " r'\\bfastest\\b': 'fast',\n",
119
+ "}\n",
120
+ "EMOJI_RX = re.compile(r'[\\U0001F300-\\U0001FAFF\\U00002700-\\U000027BF]')\n",
121
+ "HASHTAG_RX = re.compile(r'(#\\w+)')\n",
122
+ "SPACE_RX = re.compile(r'\\s+')\n",
123
+ "\n",
124
+ "def _replace_banned(text: str, audit: List[str]) -> str:\n",
125
+ " new = text\n",
126
+ " for pat, repl in BANNED_MAP.items():\n",
127
+ " if re.search(pat, new, flags=re.I):\n",
128
+ " new = re.sub(pat, repl, new, flags=re.I)\n",
129
+ " audit.append(f\"Replaced banned phrasing '{pat}' -> '{repl}'.\")\n",
130
+ " return new\n",
131
+ "\n",
132
+ "def _limit_hashtags(s: str, max_tags: int, audit: List[str]) -> str:\n",
133
+ " tags = HASHTAG_RX.findall(s)\n",
134
+ " if max_tags == 0 and tags:\n",
135
+ " s2 = HASHTAG_RX.sub('', s)\n",
136
+ " s2 = SPACE_RX.sub(' ', s2).strip()\n",
137
+ " audit.append('Removed all hashtags per platform rules.')\n",
138
+ " return s2\n",
139
+ " if len(tags) <= max_tags:\n",
140
+ " return s\n",
141
+ " count = 0\n",
142
+ " toks = s.split()\n",
143
+ " for i, tok in enumerate(toks):\n",
144
+ " if tok.startswith('#'):\n",
145
+ " count += 1\n",
146
+ " if count > max_tags:\n",
147
+ " toks[i] = ''\n",
148
+ " s2 = ' '.join(t for t in toks if t)\n",
149
+ " audit.append(f'Trimmed hashtags to <= {max_tags}.')\n",
150
+ " return s2\n",
151
+ "\n",
152
+ "def _limit_emojis(s: str, max_emojis: int, audit: List[str]) -> str:\n",
153
+ " if max_emojis < 0:\n",
154
+ " return s\n",
155
+ " emojis = EMOJI_RX.findall(s)\n",
156
+ " if len(emojis) <= max_emojis:\n",
157
+ " return s\n",
158
+ " kept = 0; out = []\n",
159
+ " for ch in s:\n",
160
+ " if EMOJI_RX.match(ch):\n",
161
+ " if kept < max_emojis:\n",
162
+ " out.append(ch); kept += 1\n",
163
+ " else:\n",
164
+ " out.append(ch)\n",
165
+ " audit.append(f'Trimmed emojis to <= {max_emojis}.')\n",
166
+ " return ''.join(out)\n",
167
+ "\n",
168
+ "def _ensure_keywords(s: str, keywords: List[str], cap: int, audit: List[str]) -> str:\n",
169
+ " text = s\n",
170
+ " for kw in (keywords or []):\n",
171
+ " if kw.strip() and re.search(re.escape(kw), text, flags=re.I) is None:\n",
172
+ " cand = (text + ' ' + kw).strip()\n",
173
+ " if len(cand) <= cap:\n",
174
+ " text = cand\n",
175
+ " else:\n",
176
+ " parts = text.split()\n",
177
+ " if parts:\n",
178
+ " parts[-1] = kw\n",
179
+ " text = ' '.join(parts)\n",
180
+ " audit.append(f'Inserted missing keyword: {kw}')\n",
181
+ " return text\n",
182
+ "\n",
183
+ "def _pick_cta(cta_strength: str) -> str:\n",
184
+ " bank = {\n",
185
+ " 'soft': ['Learn more','See how it works','Try it free'],\n",
186
+ " 'medium': ['Get started today','Start now'],\n",
187
+ " 'hard': ['Start your free trial now','Buy now','Sign up now'],\n",
188
+ " }.get(cta_strength, ['Learn more'])\n",
189
+ " return bank[0]\n",
190
+ "\n",
191
+ "def _ensure_cta(text: str, cta_strength: str, audit: List[str]) -> str:\n",
192
+ " if re.search(r'\\b(learn more|start|try|buy|sign up|get started|discover|explore)\\b', text, flags=re.I):\n",
193
+ " return text\n",
194
+ " cta = _pick_cta(cta_strength)\n",
195
+ " audit.append(f\"Added CTA: '{cta}'.\")\n",
196
+ " return (text + (' ' if not text.endswith(('.', '!', '?')) else ' ') + cta).strip()\n",
197
+ "\n",
198
+ "def _smart_trim(text: str, cap: int, preserve_tail: str = '') -> str:\n",
199
+ " if len(text) <= cap:\n",
200
+ " return text\n",
201
+ " reserve = len(preserve_tail) + (1 if preserve_tail and not text.endswith(' ') else 0)\n",
202
+ " hard = max(0, cap - reserve)\n",
203
+ " trimmed = text[:hard].rstrip()\n",
204
+ " if preserve_tail:\n",
205
+ " if not trimmed.endswith(('.', '!', '?')):\n",
206
+ " trimmed = trimmed.rstrip(',;:-')\n",
207
+ " trimmed = (trimmed + ' ' + preserve_tail).strip()\n",
208
+ " return trimmed[:cap]\n",
209
+ "\n",
210
+ "def apply_validators(text: str, platform: str, cap: int, cta_strength: str, keywords: List[str]):\n",
211
+ " audit: List[str] = []\n",
212
+ " s = text.strip()\n",
213
+ " s = _replace_banned(s, audit)\n",
214
+ " s = _ensure_cta(s, cta_strength, audit)\n",
215
+ " s = _ensure_keywords(s, keywords, 10**9, audit)\n",
216
+ " tail_cta = ''\n",
217
+ " m = re.search(r'(learn more|start your free trial(?: now)?|start now|try it free|get started(?: today)?|buy now|sign up(?: now)?)$', s, flags=re.I)\n",
218
+ " if m:\n",
219
+ " tail_cta = m.group(0)\n",
220
+ " s = _smart_trim(s, cap, preserve_tail=tail_cta)\n",
221
+ " s = _ensure_keywords(s, keywords, cap, audit)\n",
222
+ " rule = PLATFORM_RULES.get(platform, PLATFORM_RULES['Instagram'])\n",
223
+ " s = _limit_hashtags(s, rule['hashtags_max'], audit)\n",
224
+ " s = _limit_emojis(s, rule['emoji_max'], audit)\n",
225
+ " if len(s) > cap:\n",
226
+ " s = s[:cap].rstrip()\n",
227
+ " audit.append(f'Force-clipped to {cap} chars.')\n",
228
+ " return s, audit\n"
229
+ ]
230
+ },
231
+ {
232
+ "cell_type": "code",
233
+ "execution_count": null,
234
+ "metadata": {},
235
+ "outputs": [],
236
+ "source": [
237
+ "class ShortWindowMemory:\n",
238
+ " def __init__(self, k: int = 3):\n",
239
+ " self.window = deque(maxlen=k)\n",
240
+ " def add(self, user: str, assistant: str):\n",
241
+ " self.window.append({'user': user, 'assistant': assistant})\n",
242
+ " def text(self) -> str:\n",
243
+ " if not self.window:\n",
244
+ " return ''\n",
245
+ " lines = []\n",
246
+ " for t in self.window:\n",
247
+ " lines.append(f\"Human: {t['user']}\")\n",
248
+ " lines.append(f\"AI: {t['assistant']}\")\n",
249
+ " return '\\n'.join(lines)\n",
250
+ " def reset(self):\n",
251
+ " self.window.clear()\n"
252
+ ]
253
+ },
254
+ {
255
+ "cell_type": "code",
256
+ "execution_count": null,
257
+ "metadata": {},
258
+ "outputs": [],
259
+ "source": [
260
+ "SESSION_PROFILE: Dict[str, Any] = {\n",
261
+ " 'brand': '', 'product': '', 'audience': '', 'voice': '', 'facts': {}\n",
262
+ "}\n",
263
+ "def remember(k: str, v: str):\n",
264
+ " if k in ('brand','product','audience','voice'):\n",
265
+ " SESSION_PROFILE[k] = v\n",
266
+ " else:\n",
267
+ " SESSION_PROFILE['facts'][k] = v\n",
268
+ " return SESSION_PROFILE\n",
269
+ "def forget(k: str):\n",
270
+ " if k in ('brand','product','audience','voice'):\n",
271
+ " SESSION_PROFILE[k] = ''\n",
272
+ " else:\n",
273
+ " SESSION_PROFILE['facts'].pop(k, None)\n",
274
+ " return SESSION_PROFILE\n",
275
+ "def facts_dump() -> str:\n",
276
+ " f = [f\"- brand: {SESSION_PROFILE.get('brand','')}\",\n",
277
+ " f\"- product: {SESSION_PROFILE.get('product','')}\",\n",
278
+ " f\"- audience: {SESSION_PROFILE.get('audience','')}\",\n",
279
+ " f\"- voice: {SESSION_PROFILE.get('voice','')}\"]\n",
280
+ " if SESSION_PROFILE['facts']:\n",
281
+ " f.append('- facts:')\n",
282
+ " for k,v in SESSION_PROFILE['facts'].items():\n",
283
+ " f.append(f\" • {k}: {v}\")\n",
284
+ " return '\\n'.join(f)\n",
285
+ "BASE_GUIDANCE = (\n",
286
+ " 'You are Marketeer, a concise, benefit-first marketing copywriter. '\n",
287
+ " 'Respect the platform\\'s character cap, include required keywords, and end with a clear but compliant CTA. '\n",
288
+ " \"Avoid absolute claims like 'guaranteed', '#1', or 'best'. Prefer modest, evidence-backed phrasing.\"\n",
289
+ ")\n",
290
+ "def _profile_block() -> str:\n",
291
+ " lines = []\n",
292
+ " if any(SESSION_PROFILE.values()):\n",
293
+ " lines.append('Session profile:')\n",
294
+ " if SESSION_PROFILE.get('brand'): lines.append(f\"- Brand: {SESSION_PROFILE['brand']}\")\n",
295
+ " if SESSION_PROFILE.get('product'): lines.append(f\"- Product: {SESSION_PROFILE['product']}\")\n",
296
+ " if SESSION_PROFILE.get('audience'):lines.append(f\"- Audience: {SESSION_PROFILE['audience']}\")\n",
297
+ " if SESSION_PROFILE.get('voice'): lines.append(f\"- Voice: {SESSION_PROFILE['voice']}\")\n",
298
+ " if SESSION_PROFILE['facts']:\n",
299
+ " lines.append('- Key facts:')\n",
300
+ " for k,v in SESSION_PROFILE['facts'].items():\n",
301
+ " lines.append(f\" • {k}: {v}\")\n",
302
+ " return '\\n'.join(lines) if lines else '[no session profile]'\n",
303
+ "import textwrap\n",
304
+ "def build_prompt(user_input: str, platform: str, tone: str, cta_strength: str, cap: int,\n",
305
+ " keywords: List[str], history_text: str = '') -> str:\n",
306
+ " kw = ', '.join(keywords) if keywords else '(none)'\n",
307
+ " profile = _profile_block()\n",
308
+ " return textwrap.dedent(f\"\"\"\n",
309
+ " {BASE_GUIDANCE}\n",
310
+ "\n",
311
+ " Context (recent conversation, if any):\n",
312
+ " {history_text if history_text else '[no prior turns in memory]'}\n",
313
+ "\n",
314
+ " {profile}\n",
315
+ "\n",
316
+ " Task:\n",
317
+ " - Platform: {platform}\n",
318
+ " - Tone: {tone}\n",
319
+ " - CTA strength: {cta_strength}\n",
320
+ " - Character cap: {cap}\n",
321
+ " - Required keywords: {kw}\n",
322
+ "\n",
323
+ " User request:\n",
324
+ " {user_input}\n",
325
+ "\n",
326
+ " Instructions:\n",
327
+ " - Be benefit-first and platform-appropriate.\n",
328
+ " - Keep within the character cap (hard limit {cap} chars).\n",
329
+ " - Include all required keywords (if any).\n",
330
+ " - Close with a clear CTA matching CTA strength.\n",
331
+ " - Avoid banned claims ('guaranteed', '#1', 'best').\n",
332
+ "\n",
333
+ " Return only the marketing copy (no preamble).\n",
334
+ " \"\"\").strip()\n"
335
+ ]
336
+ },
337
+ {
338
+ "cell_type": "code",
339
+ "execution_count": null,
340
+ "metadata": {},
341
+ "outputs": [],
342
+ "source": [
343
+ "import getpass\n",
344
+ "MODEL_ID = 'google/gemma-2-2b-it'\n",
345
+ "DTYPE = 'bfloat16'\n",
346
+ "try:\n",
347
+ " token = getpass.getpass('Enter HF token (press Enter to skip): ')\n",
348
+ " HF_TOKEN = token.strip() or None\n",
349
+ "except Exception:\n",
350
+ " HF_TOKEN = None\n",
351
+ "torch_dtype = {'bfloat16': torch.bfloat16, 'float16': torch.float16}.get(DTYPE, torch.bfloat16)\n",
352
+ "tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)\n",
353
+ "if tokenizer.pad_token is None:\n",
354
+ " tokenizer.pad_token = tokenizer.eos_token\n",
355
+ "model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch_dtype, device_map='auto', token=HF_TOKEN)\n",
356
+ "print({'model': MODEL_ID, 'dtype': str(torch_dtype).replace('torch.', '')})"
357
+ ]
358
+ },
359
+ {
360
+ "cell_type": "code",
361
+ "execution_count": null,
362
+ "metadata": {},
363
+ "outputs": [],
364
+ "source": [
365
+ "memory = ShortWindowMemory(k=3)\n",
366
+ "DEFAULTS = {'platform': 'Instagram', 'tone': 'friendly, energetic', 'cta': 'soft'}\n",
367
+ "def _generate(prompt_text: str, max_new_tokens=180, temperature=0.7, top_p=0.9, repetition_penalty=1.1):\n",
368
+ " messages = [{'role': 'user', 'content': prompt_text}]\n",
369
+ " input_ids = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors='pt').to(model.device)\n",
370
+ " with torch.no_grad():\n",
371
+ " out = model.generate(\n",
372
+ " input_ids=input_ids,\n",
373
+ " max_new_tokens=max_new_tokens,\n",
374
+ " temperature=temperature,\n",
375
+ " top_p=top_p,\n",
376
+ " repetition_penalty=repetition_penalty,\n",
377
+ " do_sample=True,\n",
378
+ " pad_token_id=tokenizer.pad_token_id,\n",
379
+ " eos_token_id=tokenizer.eos_token_id,\n",
380
+ " )\n",
381
+ " gen_ids = out[0][input_ids.shape[-1]:]\n",
382
+ " return tokenizer.decode(gen_ids, skip_special_tokens=True).strip()\n",
383
+ "def send(user: str, platform: str=None, tone: str=None, cta_strength: str=None, cap: int=None, keywords: List[str]=None,\n",
384
+ " max_new_tokens=180, temperature=0.7, top_p=0.9, repetition_penalty=1.1):\n",
385
+ " platform = platform or DEFAULTS['platform']\n",
386
+ " tone = tone or DEFAULTS['tone']\n",
387
+ " cta_strength = cta_strength or DEFAULTS['cta']\n",
388
+ " cap = cap or PLATFORM_RULES.get(platform, PLATFORM_RULES['Instagram'])['cap']\n",
389
+ " keywords = keywords or []\n",
390
+ " prompt_text = build_prompt(user, platform, tone, cta_strength, cap, keywords, memory.text())\n",
391
+ " raw = _generate(prompt_text, max_new_tokens, temperature, top_p, repetition_penalty)\n",
392
+ " final, audit = apply_validators(raw, platform, cap, cta_strength, keywords)\n",
393
+ " memory.add(user, final)\n",
394
+ " return {'raw': raw, 'final': final, 'audit': audit, 'cap': cap}\n"
395
+ ]
396
+ },
397
+ {
398
+ "cell_type": "code",
399
+ "execution_count": null,
400
+ "metadata": {},
401
+ "outputs": [],
402
+ "source": [
403
+ "VIDEO_BLUEPRINTS = {\n",
404
+ " 'short_ad': ['Hook (2-3s)','Problem (3-5s)','Product intro (4-6s)','Key benefit (4-6s)','CTA (2-3s)'],\n",
405
+ " 'ugc_review': ['Relatable hook','Pain point','Discovery','Feature demo','Social proof','CTA'],\n",
406
+ " 'how_to': ['Teaser result','Step 1','Step 2','Step 3','Recap + CTA'],\n",
407
+ "}\n",
408
+ "def plan_video(blueprint: str='short_ad', duration_sec: int=20, product_brief: str='') -> Dict[str, Any]:\n",
409
+ " if blueprint not in VIDEO_BLUEPRINTS:\n",
410
+ " raise ValueError(f\"Unknown blueprint '{blueprint}'. Options: {list(VIDEO_BLUEPRINTS.keys())}\")\n",
411
+ " beats = VIDEO_BLUEPRINTS[blueprint]\n",
412
+ " per_beat = max(2, duration_sec // max(1, len(beats)))\n",
413
+ " plan = []\n",
414
+ " for i, beat in enumerate(beats, 1):\n",
415
+ " plan.append({\n",
416
+ " 'order': i,\n",
417
+ " 'beat': beat,\n",
418
+ " 'time_window': f\"{(i-1)*per_beat:02d}-{min(i*per_beat, duration_sec):02d}s\",\n",
419
+ " })\n",
420
+ " return {\n",
421
+ " 'blueprint': blueprint,\n",
422
+ " 'duration_sec': duration_sec,\n",
423
+ " 'beats': plan,\n",
424
+ " 'product_brief': product_brief or '(use chat context)',\n",
425
+ " }\n"
426
+ ]
427
+ },
428
+ {
429
+ "cell_type": "code",
430
+ "execution_count": null,
431
+ "metadata": {},
432
+ "outputs": [],
433
+ "source": [
434
+ "import json as _json, re as _re\n",
435
+ "\n",
436
+ "def _session_context_text():\n",
437
+ " history_text = memory.text()\n",
438
+ " lines = []\n",
439
+ " lines.append('=== SESSION PROFILE ===')\n",
440
+ " if SESSION_PROFILE.get('brand'): lines.append(f\"Brand: {SESSION_PROFILE['brand']}\")\n",
441
+ " if SESSION_PROFILE.get('product'): lines.append(f\"Product: {SESSION_PROFILE['product']}\")\n",
442
+ " if SESSION_PROFILE.get('audience'):lines.append(f\"Audience: {SESSION_PROFILE['audience']}\")\n",
443
+ " if SESSION_PROFILE.get('voice'): lines.append(f\"Preferred Voice: {SESSION_PROFILE['voice']}\")\n",
444
+ " if SESSION_PROFILE.get('facts'):\n",
445
+ " lines.append('Facts:')\n",
446
+ " for k,v in SESSION_PROFILE['facts'].items():\n",
447
+ " lines.append(f\"- {k}: {v}\")\n",
448
+ " lines.append('\\n=== RECENT CONVERSATION ===')\n",
449
+ " lines.append(history_text if history_text else '[none]')\n",
450
+ " return '\\n'.join(lines)\n",
451
+ "\n",
452
+ "def _decode_tail(out_ids, start_idx):\n",
453
+ " return tokenizer.decode(out_ids[0][start_idx:], skip_special_tokens=True).strip()\n",
454
+ "\n",
455
+ "def _just_user_prompt(prompt_text: str, max_new_tokens=320):\n",
456
+ " messages = [{'role':'user','content': prompt_text}]\n",
457
+ " input_ids = tokenizer.apply_chat_template(\n",
458
+ " messages, tokenize=True, add_generation_prompt=True, return_tensors='pt'\n",
459
+ " ).to(model.device)\n",
460
+ " with torch.no_grad():\n",
461
+ " out = model.generate(\n",
462
+ " input_ids=input_ids,\n",
463
+ " max_new_tokens=max_new_tokens,\n",
464
+ " temperature=0.6,\n",
465
+ " top_p=0.9,\n",
466
+ " repetition_penalty=1.1,\n",
467
+ " do_sample=True,\n",
468
+ " pad_token_id=tokenizer.pad_token_id,\n",
469
+ " eos_token_id=tokenizer.eos_token_id,\n",
470
+ " )\n",
471
+ " return _decode_tail(out, input_ids.shape[-1])\n",
472
+ "\n",
473
+ "def _extract_json(gen_text: str):\n",
474
+ " try:\n",
475
+ " return _json.loads(gen_text)\n",
476
+ " except Exception:\n",
477
+ " pass\n",
478
+ " m = _re.search(r'\\{.*\\}', gen_text, flags=_re.S)\n",
479
+ " if m:\n",
480
+ " try:\n",
481
+ " return _json.loads(m.group(0))\n",
482
+ " except Exception:\n",
483
+ " pass\n",
484
+ " return None\n",
485
+ "\n",
486
+ "def _fallback_block(beat_title: str):\n",
487
+ " short = beat_title.split('(')[0].strip()\n",
488
+ " return {\n",
489
+ " 'voiceover': f\"{short}: naturally delicious, try it today.\",\n",
490
+ " 'on_screen': (short[:32] or 'Fresh & Natural'),\n",
491
+ " 'shots': ['Close-up product', 'Serving scoop', 'Happy bite'],\n",
492
+ " 'broll': ['Farm/ingredient cutaways', 'Pouring/serving'],\n",
493
+ " 'captions': ['Naturally made ice cream', 'From local farms'],\n",
494
+ " }\n",
495
+ "\n",
496
+ "def script_video_from_plan(plan: dict, style: str = 'friendly, energetic', platform: str = 'Instagram', debug_first=False):\n",
497
+ " context = _session_context_text()\n",
498
+ " blueprint = plan.get('blueprint', '?')\n",
499
+ " duration = plan.get('duration_sec', 20)\n",
500
+ " beats = plan.get('beats', [])\n",
501
+ " scripted_beats = []\n",
502
+ "\n",
503
+ " for idx, b in enumerate(beats):\n",
504
+ " brief = (\n",
505
+ " context + '\\n\\n'\n",
506
+ " '=== VIDEO BLUEPRINT ===\\n'\n",
507
+ " f'Type: {blueprint} | Duration: {duration}s\\n'\n",
508
+ " f\"Current beat: {b['order']} — {b['beat']} ({b['time_window']})\\n\\n\"\n",
509
+ " 'Write concise items with the following constraints:\\n'\n",
510
+ " '- voiceover: <= 18 words, benefits-first, natural.\\n'\n",
511
+ " '- on_screen: <= 36 characters, punchy overlay text.\\n'\n",
512
+ " '- shots: 3 ideas, short imperatives (e.g., \"Close-up pour\").\\n'\n",
513
+ " '- broll: 2 ideas, short.\\n'\n",
514
+ " '- captions: 1–2 lines, each <= 40 characters.\\n\\n'\n",
515
+ " f'Platform: {platform}\\n'\n",
516
+ " f'Style/Tone: {style}\\n\\n'\n",
517
+ " 'Return ONLY valid JSON with keys:\\n'\n",
518
+ " '{\\n \"voiceover\": \"string\",\\n \"on_screen\": \"string\",\\n \"shots\": [\"...\", \"...\", \"...\"],\\n \"broll\": [\"...\", \"...\"],\\n \"captions\": [\"...\", \"...\"]\\n}'\n",
519
+ " )\n",
520
+ " gen_text = _just_user_prompt(brief, max_new_tokens=320)\n",
521
+ " if debug_first and idx == 0:\n",
522
+ " print('RAW (beat 1):\\n', gen_text)\n",
523
+ " data = _extract_json(gen_text)\n",
524
+ " if not data:\n",
525
+ " data = _fallback_block(b.get('beat','Beat'))\n",
526
+ "\n",
527
+ " scripted_beats.append({\n",
528
+ " 'order': b.get('order'),\n",
529
+ " 'time_window': b.get('time_window'),\n",
530
+ " 'beat': b.get('beat'),\n",
531
+ " 'voiceover': data.get('voiceover','') or _fallback_block(b.get('beat',''))['voiceover'],\n",
532
+ " 'on_screen': data.get('on_screen','') or _fallback_block(b.get('beat',''))['on_screen'],\n",
533
+ " 'shots': (data.get('shots') or _fallback_block(b.get('beat',''))['shots'])[:3],\n",
534
+ " 'broll': (data.get('broll') or _fallback_block(b.get('beat',''))['broll'])[:2],\n",
535
+ " 'captions': (data.get('captions') or _fallback_block(b.get('beat',''))['captions'])[:2],\n",
536
+ " })\n",
537
+ "\n",
538
+ " return {\n",
539
+ " 'blueprint': blueprint,\n",
540
+ " 'duration_sec': duration,\n",
541
+ " 'style': style,\n",
542
+ " 'platform': platform,\n",
543
+ " 'product_brief': plan.get('product_brief',''),\n",
544
+ " 'script': scripted_beats\n",
545
+ " }\n",
546
+ "\n",
547
+ "def make_video(plan_or_blueprint='short_ad', duration=20, product_brief='', style='friendly, energetic', platform='Instagram', debug_first=False):\n",
548
+ " if isinstance(plan_or_blueprint, dict):\n",
549
+ " plan = plan_or_blueprint\n",
550
+ " else:\n",
551
+ " plan = plan_video(plan_or_blueprint, duration, product_brief)\n",
552
+ " return script_video_from_plan(plan, style=style, platform=platform, debug_first=debug_first)\n"
553
+ ]
554
+ },
555
+ {
556
+ "cell_type": "code",
557
+ "execution_count": null,
558
+ "metadata": {},
559
+ "outputs": [],
560
+ "source": [
561
+ "from rich.console import Console\n",
562
+ "from rich.table import Table\n",
563
+ "from rich.panel import Panel\n",
564
+ "from rich.markdown import Markdown\n",
565
+ "import re, json as _j\n",
566
+ "\n",
567
+ "console = Console()\n",
568
+ "def _print_header():\n",
569
+ " t = Table(title='Marketeer — REPL (Patched)', show_lines=False)\n",
570
+ " t.add_column('Setting', style='cyan', no_wrap=True)\n",
571
+ " t.add_column('Value', style='white')\n",
572
+ " t.add_row('Model', MODEL_ID)\n",
573
+ " t.add_row('Platform', 'Instagram')\n",
574
+ " t.add_row('Tone', 'friendly, energetic')\n",
575
+ " t.add_row('CTA', 'soft')\n",
576
+ " console.print(t)\n",
577
+ " console.print(Markdown(\n",
578
+ " '**Commands**\\n'\n",
579
+ " '- `/remember key=value`, `/forget key`, `/facts`\\n'\n",
580
+ " '- `/video blueprint=short_ad duration=20 style=warm platform=Instagram`\\n'\n",
581
+ " '- Type any prompt to generate copy.'\n",
582
+ " ))\n",
583
+ "\n",
584
+ "def _parse_kv(line: str):\n",
585
+ " parts = re.findall(r'(\\w+)=(\".*?\"|\\'.*?\\'|\\S+)', line)\n",
586
+ " out = {}\n",
587
+ " for k, v in parts:\n",
588
+ " v = v.strip().strip('\"').strip(\"'\")\n",
589
+ " out[k] = v\n",
590
+ " return out\n",
591
+ "\n",
592
+ "def repl():\n",
593
+ " _print_header()\n",
594
+ " while True:\n",
595
+ " try:\n",
596
+ " line = console.input('[bold magenta]You[/bold magenta]: ').strip()\n",
597
+ " except (KeyboardInterrupt, EOFError):\n",
598
+ " console.print('\\n[yellow]Bye.[/yellow]'); break\n",
599
+ " if not line:\n",
600
+ " continue\n",
601
+ " low = line.lower()\n",
602
+ " if low in ('/exit','exit','quit','/quit'):\n",
603
+ " console.print('[yellow]Bye.[/yellow]'); break\n",
604
+ " if low.startswith('/facts'):\n",
605
+ " console.print(Panel(facts_dump() or '(no facts)', title='Session Facts')); continue\n",
606
+ " if low.startswith('/forget'):\n",
607
+ " kv = _parse_kv(line)\n",
608
+ " for k in kv.keys(): forget(k)\n",
609
+ " console.print(Panel('Updated facts.', title='OK')); continue\n",
610
+ " if low.startswith('/remember'):\n",
611
+ " kv = _parse_kv(line)\n",
612
+ " if not kv and '=' in line:\n",
613
+ " raw = line.split(None, 1)[1]\n",
614
+ " k,v = raw.split('=',1)\n",
615
+ " remember(k.strip(), v.strip())\n",
616
+ " else:\n",
617
+ " for k,v in kv.items(): remember(k, v)\n",
618
+ " console.print(Panel('Saved.', title='OK')); continue\n",
619
+ " if low.startswith('/video'):\n",
620
+ " kv = _parse_kv(line)\n",
621
+ " blueprint = kv.get('blueprint','short_ad')\n",
622
+ " duration = int(kv.get('duration','20'))\n",
623
+ " style = kv.get('style','friendly, energetic')\n",
624
+ " platformV = kv.get('platform','Instagram')\n",
625
+ " brief = SESSION_PROFILE.get('product','') or SESSION_PROFILE.get('brand','') or 'marketing video'\n",
626
+ " plan = plan_video(blueprint, duration, brief)\n",
627
+ " script = make_video(plan, style=style, platform=platformV, debug_first=True)\n",
628
+ " console.print(Panel(_j.dumps(script, indent=2), title='Video Script'))\n",
629
+ " continue\n",
630
+ " # normal prompt\n",
631
+ " result = send(line)\n",
632
+ " console.print(Panel(result['final'], title='Response', subtitle=f\"len={len(result['final'])}/{result['cap']}\"))\n",
633
+ " if result['audit']:\n",
634
+ " md = '\\n'.join([f\"- {a}\" for a in result['audit']])\n",
635
+ " console.print(Panel(md, title='Audit trail'))\n",
636
+ " else:\n",
637
+ " console.print('[dim]No edits needed.[/dim]')\n",
638
+ "\n",
639
+ "repl()\n"
640
+ ]
641
+ },
642
+ {
643
+ "cell_type": "markdown",
644
+ "metadata": {},
645
+ "source": [
646
+ "## Quick self‑test (optional)"
647
+ ]
648
+ },
649
+ {
650
+ "cell_type": "code",
651
+ "execution_count": null,
652
+ "metadata": {},
653
+ "outputs": [],
654
+ "source": [
655
+ "# Uncomment to smoke test video scripting\n",
656
+ "# remember('brand','FrostFields')\n",
657
+ "# remember('product','Natural fruit ice cream')\n",
658
+ "# remember('origin','Local farms')\n",
659
+ "# plan = plan_video('short_ad', 20, 'Natural ice cream with coconut & apple')\n",
660
+ "# vid = make_video(plan, style='warm, wholesome', platform='Instagram', debug_first=True)\n",
661
+ "# import json as j; print(j.dumps(vid, indent=2))\n"
662
+ ]
663
+ }
664
+ ],
665
+ "metadata": {
666
+ "colab": {
667
+ "name": "Marketeer_Patched_Video.ipynb"
668
+ },
669
+ "kernelspec": {
670
+ "display_name": "ai",
671
+ "language": "python",
672
+ "name": "python3"
673
+ },
674
+ "language_info": {
675
+ "codemirror_mode": {
676
+ "name": "ipython",
677
+ "version": 3
678
+ },
679
+ "file_extension": ".py",
680
+ "mimetype": "text/x-python",
681
+ "name": "python",
682
+ "nbconvert_exporter": "python",
683
+ "pygments_lexer": "ipython3",
684
+ "version": "3.10.11"
685
+ }
686
+ },
687
+ "nbformat": 4,
688
+ "nbformat_minor": 5
689
+ }
README.md CHANGED
@@ -1,12 +1 @@
1
- ---
2
- title: Marketeer
3
- emoji: 🌖
4
- colorFrom: purple
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.0.2
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # Marketing_chat_bot
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ App entry point for Marketeer on Hugging Face Spaces (and local use).
3
+ """
4
+
5
+ from ui.gradio_ui import create_interface
6
+
7
+
8
+ # Hugging Face Spaces will run this file.
9
+ # Locally, you can run: python app.py
10
+ if __name__ == "__main__":
11
+ demo = create_interface()
12
+ # On Spaces, you typically don't need any special args.
13
+ demo.launch()
blueprint.md ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Nice, let’s turn all that into a **clean roadmap** you can actually work through, phase by phase. Think of this as the “Marketeer x LangChain x HF” blueprint you can keep coming back to.
2
+
3
+ ---
4
+
5
+ # 🧭 Marketeer Roadmap (LangChain + Hugging Face)
6
+
7
+ ## Phase 0 – Current Baseline (where you are now)
8
+
9
+ **You already have:**
10
+
11
+ * Local HF model (Gemma) running on RTX 3060.
12
+ * Core modules:
13
+
14
+ * `core_logic/llm_client.py` (manual `generate_text`)
15
+ * `core_logic/copy_pipeline.py` (template-based copy + validators)
16
+ * `core_logic/video_pipeline.py` (video beats + parsing + validators)
17
+ * `ui/gradio_ui.py` (Gradio UI: Copy tab + Video tab)
18
+ * Simple chat mode using:
19
+
20
+ * `core_logic/chat_chain.py` (PromptTemplate + our custom MarketeerLLM / now simplified)
21
+ * Platform rules + validators from the notebook.
22
+
23
+ We’ll **evolve this** instead of throwing it away.
24
+
25
+ ---
26
+
27
+ ## Phase 1 – Switch to ChatHuggingFace Backend
28
+
29
+ **Goal:** Use **official LangChain-HuggingFace** integration instead of a custom wrapper, so future features (tools, structured outputs, etc.) are easier.
30
+
31
+ ### 1.1. Dependencies
32
+
33
+ * Add to `requirements.txt`:
34
+
35
+ ```txt
36
+ langchain-huggingface
37
+ langchain-core
38
+ transformers
39
+ accelerate
40
+ bitsandbytes
41
+ ```
42
+
43
+ ### 1.2. LLM config module
44
+
45
+ Create something like `core_logic/llm_config.py`:
46
+
47
+ * **Local dev (pipeline-based):**
48
+
49
+ ```python
50
+ from langchain_huggingface import ChatHuggingFace, HuggingFacePipeline
51
+ from transformers import BitsAndBytesConfig
52
+
53
+ def get_local_chat_model():
54
+ quant_config = BitsAndBytesConfig(
55
+ load_in_4bit=True,
56
+ bnb_4bit_quant_type="nf4",
57
+ bnb_4bit_compute_dtype="float16",
58
+ bnb_4bit_use_double_quant=True,
59
+ )
60
+
61
+ base_llm = HuggingFacePipeline.from_model_id(
62
+ model_id="google/gemma-2-2b-it",
63
+ task="text-generation",
64
+ pipeline_kwargs=dict(
65
+ max_new_tokens=256,
66
+ do_sample=True,
67
+ temperature=0.8,
68
+ top_p=0.9,
69
+ return_full_text=False,
70
+ ),
71
+ model_kwargs={"quantization_config": quant_config},
72
+ )
73
+
74
+ return ChatHuggingFace(llm=base_llm)
75
+ ```
76
+
77
+ * Later we’ll add a `get_endpoint_chat_model()` for Spaces.
78
+
79
+ ### 1.3. Deprecate `MarketeerLLM`
80
+
81
+ * Keep `core_logic/llm_client.py` around if other code still uses it.
82
+ * For chat + new logic, use `ChatHuggingFace` from `llm_config.get_local_chat_model()`.
83
+
84
+ **Deliverable:**
85
+ A single `ChatHuggingFace` object you can import anywhere as your main chat LLM.
86
+
87
+ ---
88
+
89
+ ## Phase 2 – Proper Chat Chain with System + History Messages
90
+
91
+ **Goal:** Make the copy chat flow use **LangChain-style messages** instead of raw strings, so context handling is cleaner.
92
+
93
+ ### 2.1. New chat chain file
94
+
95
+ Refactor `core_logic/chat_chain.py`:
96
+
97
+ * Import:
98
+
99
+ ```python
100
+ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
101
+ from langchain_core.prompts import ChatPromptTemplate
102
+ from core_logic.llm_config import get_local_chat_model
103
+ from helpers.platform_rules import ...
104
+ from helpers.validators import validate_and_edit
105
+ ```
106
+
107
+ * Build a **System prompt** from campaign context + platform rules:
108
+
109
+ ```python
110
+ def build_system_message(req, platform_cfg):
111
+ content = f"""
112
+ You are an expert social media marketer.
113
+
114
+ Brand: {req.brand}
115
+ Product/Offer: {req.product}
116
+ Target audience: {req.audience}
117
+ Campaign goal: {req.goal}
118
+ Platform: {platform_cfg.name}
119
+ Tone: {req.tone}
120
+ CTA style: {req.cta_style}
121
+ Extra context: {req.extra_context}
122
+
123
+ Follow platform rules and keep the final post within ~{platform_cfg.char_cap} characters.
124
+ Respond ONLY with the post text (no explanations).
125
+ """
126
+ return SystemMessage(content=content.strip())
127
+ ```
128
+
129
+ * Convert `chat_history` (list `[user, assistant]`) to messages:
130
+
131
+ ```python
132
+ def history_to_messages(history_pairs):
133
+ msgs = []
134
+ for u, a in history_pairs:
135
+ if u:
136
+ msgs.append(HumanMessage(content=u))
137
+ if a:
138
+ msgs.append(AIMessage(content=a))
139
+ return msgs
140
+ ```
141
+
142
+ * `chat_turn(...)`:
143
+
144
+ ```python
145
+ def chat_turn(req, user_message, history_pairs):
146
+ platform_cfg = _get_platform_config(req.platform_name)
147
+ chat_model = get_local_chat_model()
148
+
149
+ system_msg = build_system_message(req, platform_cfg)
150
+ history_msgs = history_to_messages(history_pairs)
151
+ user_msg = HumanMessage(content=user_message)
152
+
153
+ messages = [system_msg] + history_msgs + [user_msg]
154
+
155
+ ai_msg = chat_model.invoke(messages)
156
+ raw_text = ai_msg.content
157
+ final_text, audit = validate_and_edit(raw_text, platform_cfg)
158
+ return final_text, raw_text, audit
159
+ ```
160
+
161
+ ### 2.2. UI stays mostly same
162
+
163
+ * `ui/gradio_ui.py` continues passing `chat_history` and `user_message`.
164
+ * Internally, `chat_turn` now uses **true chat messages** and a **SystemMessage**.
165
+
166
+ **Deliverable:**
167
+ Your chat bot respects system instructions + history in a robust, LangChain-native way.
168
+
169
+ ---
170
+
171
+ ## Phase 3 – Structured Output for Video Script Generator
172
+
173
+ **Goal:** Instead of fragile JSON/beat parsing, use LangChain’s structured output to get reliable beat objects.
174
+
175
+ ### 3.1. Define a Pydantic model
176
+
177
+ In `core_logic/video_schema.py`:
178
+
179
+ ```python
180
+ from typing import List
181
+ from pydantic import BaseModel
182
+
183
+ class Beat(BaseModel):
184
+ title: str
185
+ voiceover: str
186
+ on_screen: str
187
+ shots: List[str]
188
+ broll: List[str]
189
+ captions: List[str]
190
+ t_start: float
191
+ t_end: float
192
+
193
+ class VideoPlan(BaseModel):
194
+ blueprint_name: str
195
+ duration_sec: int
196
+ platform_name: str
197
+ style: str
198
+ beats: List[Beat]
199
+ ```
200
+
201
+ ### 3.2. Use structured output in `video_pipeline`
202
+
203
+ * Build a prompt that instructs the model to output that schema.
204
+ * Use `StructuredTool` or LangChain’s `with_structured_output(VideoPlan)` (depending on version).
205
+ * Replace manual JSON parsing with a direct Pydantic model.
206
+
207
+ **Deliverable:**
208
+ `generate_video_script()` returns a `VideoPlan` object directly, with clean per-beat data and fewer parsing errors.
209
+
210
+ ---
211
+
212
+ ## Phase 4 – LangChain Memory (Optional but Powerful)
213
+
214
+ **Goal:** Advanced memory if you want more than simple chat history.
215
+
216
+ ### 4.1. Token-based memory
217
+
218
+ Use `ConversationTokenBufferMemory` instead of raw `chat_history`:
219
+
220
+ * Wrap it around your `ChatHuggingFace` model.
221
+ * Limit memory by tokens (e.g., last 1024 tokens).
222
+ * Still easy to connect to Gradio by syncing the memory with `chat_history`.
223
+
224
+ ### 4.2. Knowledge-style memory (later)
225
+
226
+ If you want persistence per brand/campaign:
227
+
228
+ * Store brand facts in a DB and summarize them per session.
229
+ * Memory retrieval each time a session starts.
230
+
231
+ **Deliverable:**
232
+ Chat sessions that scale better (longer conversations) without bloating context.
233
+
234
+ ---
235
+
236
+ ## Phase 5 – Tools & Agents for “Smart Marketing Assistant”
237
+
238
+ **Goal:** Turn Marketeer from a “single LLM” into a **tool-using assistant**.
239
+
240
+ ### 5.1. Tools
241
+
242
+ Some concrete tools:
243
+
244
+ * `generate_hashtags(copy, platform)`
245
+ * `rewrite_tone(copy, tone)`
246
+ * `check_length(copy, platform)` (wraps your validators)
247
+ * `summarize_campaign(history)`
248
+
249
+ Use LangChain’s tool abstraction (`@tool` or `Tool` class) to define them.
250
+
251
+ ### 5.2. Router / Agent
252
+
253
+ Use an agent that decides:
254
+
255
+ * When user asks “shorten this” → use `rewrite_tone`.
256
+ * When user says “give options” → generate variants.
257
+ * When user says “turn this into a video script” → call video generator.
258
+
259
+ **Deliverable:**
260
+ Single chat entry point that can:
261
+
262
+ * Write copy
263
+ * Edit copy
264
+ * Generate video storyboards
265
+ * Explain why choices were made (if you want)
266
+
267
+ ---
268
+
269
+ ## Phase 6 – Hugging Face Spaces Deployment Plan
270
+
271
+ **Goal:** Clean, reproducible deployment.
272
+
273
+ ### 6.1. Repo structure (already close, just formalize)
274
+
275
+ * `app.py` – Gradio entry
276
+ * `ui/` – UI code
277
+ * `core_logic/` – pipelines, chain logic
278
+ * `helpers/` – platform rules, validators
279
+ * `requirements.txt`
280
+ * `README.md`
281
+
282
+ ### 6.2. Backend choice for Spaces
283
+
284
+ * **Dev / personal Space:**
285
+ Use local model (pipeline) in `app.py` (like now).
286
+ * **Production / shared Space:**
287
+ Switch `llm_config.get_local_chat_model()` to `get_endpoint_chat_model()` that uses `HuggingFaceEndpoint` and reads `HUGGINGFACEHUB_API_TOKEN` from secrets.
288
+
289
+ **Deliverable:**
290
+ A Space that others can open and:
291
+
292
+ * Fill brand/product/audience/goal
293
+ * Chat with the Marketeer bot
294
+ * Generate copy + video scripts
295
+ * With stable HF-hosted backend
296
+
297
+ ---
298
+
299
+ ## Phase 7 – “Strategist Mode” (Bonus)
300
+
301
+ **Goal:** Move beyond just *copy* to *campaign thinking*.
302
+
303
+ Ideas:
304
+
305
+ * A mode that, given brand + budget + timeframe, outputs:
306
+
307
+ * channel mix (IG / LinkedIn / YouTube Shorts)
308
+ * example posts per channel
309
+ * rough posting cadence
310
+ * Use LangChain prompts with a “marketing strategist” SystemMessage.
311
+ * Let user toggle between “Copywriter Mode” and “Strategist Mode” in the UI.
312
+
313
+ **Deliverable:**
314
+ Marketeer feels like a mini marketing partner, not just a caption generator.
315
+
316
+ ---
317
+
318
+ ## How to Use This Roadmap
319
+
320
+ You can literally go phase by phase:
321
+
322
+ 1. **Phase 1** – Swap to `ChatHuggingFace` backend.
323
+ 2. **Phase 2** – Refactor `chat_chain` to use System + messages.
324
+ 3. **Phase 3** – Structured outputs for the video planner.
325
+ 4. **Phase 4+** – Optional memory / tools / agents / deployment polish.
326
+
327
+ Whenever you’re ready, tell me:
328
+
329
+ > “Let’s start Phase 1 step-by-step”
330
+
331
+ and I’ll write the exact code changes (file-by-file, minimal diff style) to get that phase done.
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
check.ipynb ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "49c5075b",
7
+ "metadata": {},
8
+ "outputs": [
9
+ {
10
+ "name": "stdout",
11
+ "output_type": "stream",
12
+ "text": [
13
+ "Tue Nov 25 03:08:08 2025 \n",
14
+ "+-----------------------------------------------------------------------------------------+\n",
15
+ "| NVIDIA-SMI 550.54.15 Driver Version: 550.54.15 CUDA Version: 12.4 |\n",
16
+ "|-----------------------------------------+------------------------+----------------------+\n",
17
+ "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n",
18
+ "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n",
19
+ "| | | MIG M. |\n",
20
+ "|=========================================+========================+======================|\n",
21
+ "| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 |\n",
22
+ "| N/A 45C P8 9W / 70W | 0MiB / 15360MiB | 0% Default |\n",
23
+ "| | | N/A |\n",
24
+ "+-----------------------------------------+------------------------+----------------------+\n",
25
+ " \n",
26
+ "+-----------------------------------------------------------------------------------------+\n",
27
+ "| Processes: |\n",
28
+ "| GPU GI CI PID Type Process name GPU Memory |\n",
29
+ "| ID ID Usage |\n",
30
+ "|=========================================================================================|\n",
31
+ "| No running processes found |\n",
32
+ "+-----------------------------------------------------------------------------------------+\n"
33
+ ]
34
+ }
35
+ ],
36
+ "source": [
37
+ "!nvidia-smi"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "a227f2ae",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": []
47
+ }
48
+ ],
49
+ "metadata": {
50
+ "kernelspec": {
51
+ "display_name": "Python 3 (ipykernel)",
52
+ "language": "python",
53
+ "name": "python3"
54
+ },
55
+ "language_info": {
56
+ "codemirror_mode": {
57
+ "name": "ipython",
58
+ "version": 3
59
+ },
60
+ "file_extension": ".py",
61
+ "mimetype": "text/x-python",
62
+ "name": "python",
63
+ "nbconvert_exporter": "python",
64
+ "pygments_lexer": "ipython3",
65
+ "version": "3.12.12"
66
+ }
67
+ },
68
+ "nbformat": 4,
69
+ "nbformat_minor": 5
70
+ }
core_logic/__init__.py ADDED
File without changes
core_logic/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (154 Bytes). View file
 
core_logic/__pycache__/chat_agent.cpython-310.pyc ADDED
Binary file (5.03 kB). View file
 
core_logic/__pycache__/chat_chain.cpython-310.pyc ADDED
Binary file (4.65 kB). View file
 
core_logic/__pycache__/copy_pipeline.cpython-310.pyc ADDED
Binary file (3.31 kB). View file
 
core_logic/__pycache__/langchain_llm.cpython-310.pyc ADDED
Binary file (1.43 kB). View file
 
core_logic/__pycache__/llm_client.cpython-310.pyc ADDED
Binary file (2.69 kB). View file
 
core_logic/__pycache__/llm_config.cpython-310.pyc ADDED
Binary file (1.41 kB). View file
 
core_logic/__pycache__/rewrite_tools.cpython-310.pyc ADDED
Binary file (1.21 kB). View file
 
core_logic/__pycache__/video_pipeline.cpython-310.pyc ADDED
Binary file (6.41 kB). View file
 
core_logic/__pycache__/video_schema.cpython-310.pyc ADDED
Binary file (3.03 kB). View file
 
core_logic/chat_agent.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent-style chat for Marketeer using LangChain tools.
3
+
4
+ - Uses ChatHuggingFace from llm_config.get_local_chat_model()
5
+ - Uses rewrite / tone tools from rewrite_tools.py
6
+ - Implements a tiny tool-calling loop with .bind_tools() (no AgentExecutor).
7
+ """
8
+
9
+ from typing import Any, Dict, List, Tuple, Union
10
+
11
+ from langchain_core.messages import (
12
+ AIMessage,
13
+ HumanMessage,
14
+ SystemMessage,
15
+ ToolMessage,
16
+ )
17
+ from langchain_core.tools import BaseTool
18
+
19
+ from core_logic.llm_config import get_local_chat_model
20
+ from core_logic.copy_pipeline import CopyRequest
21
+ from helpers.platform_styles import get_platform_style # <-- dataclass style
22
+ from core_logic.rewrite_tools import get_rewrite_tools
23
+
24
+
25
+ Message = Union[HumanMessage, AIMessage]
26
+
27
+
28
+ # --------------------------------------------------------------------
29
+ # Helpers for platform style and history
30
+ # --------------------------------------------------------------------
31
+
32
+
33
+ def _get_style_attr(style: Any, field: str, default: str = "") -> str:
34
+ """
35
+ Safe attribute getter for PlatformStyle dataclass (or dict fallback).
36
+
37
+ This handles both:
38
+ - dataclass PlatformStyle (preferred)
39
+ - dict-like (in case of accidental mix)
40
+ """
41
+ if style is None:
42
+ return default
43
+
44
+ # Dataclass / object path
45
+ if hasattr(style, field):
46
+ value = getattr(style, field)
47
+ return default if value is None else str(value)
48
+
49
+ # Dict path (just in case)
50
+ if isinstance(style, dict):
51
+ value = style.get(field, default)
52
+ return default if value is None else str(value)
53
+
54
+ return default
55
+
56
+
57
+ def _build_system_prompt(req: CopyRequest) -> str:
58
+ """
59
+ Build a system instruction that explains:
60
+ - you are a marketing copywriter
61
+ - you know the campaign context
62
+ - you may optionally use tools to rewrite/edit
63
+ """
64
+ # This comes from helpers.platform_styles and returns a PlatformStyle dataclass
65
+ style = get_platform_style(req.platform_name or "Instagram")
66
+
67
+ # Access attributes directly (NO dict-style indexing anywhere)
68
+ voice = getattr(style, "voice", "")
69
+ emoji_guideline = getattr(style, "emoji_guideline", "")
70
+ hashtag_guideline = getattr(style, "hashtag_guideline", "")
71
+ formatting_guideline = getattr(style, "formatting_guideline", "")
72
+ extra_notes = getattr(style, "extra_notes", "")
73
+
74
+ return f"""
75
+ You are Marketeer, an expert marketing copywriter.
76
+
77
+ You help users:
78
+ - write first-draft posts
79
+ - refine tone
80
+ - shorten or expand posts
81
+ - adapt copy across platforms
82
+
83
+ Campaign context:
84
+ - Brand: {req.brand}
85
+ - Product / offer: {req.product}
86
+ - Audience: {req.audience}
87
+ - Goal: {req.goal}
88
+ - Platform: {req.platform_name}
89
+ - Tone: {req.tone}
90
+ - CTA style: {req.cta_style}
91
+ - Extra context: {req.extra_context}
92
+
93
+ Platform style guidelines:
94
+ - Voice: {voice}
95
+ - Emoji usage: {emoji_guideline}
96
+ - Hashtags: {hashtag_guideline}
97
+ - Formatting: {formatting_guideline}
98
+ - Extra notes: {extra_notes}
99
+
100
+ You may have access to special tools that help you:
101
+ - adjust tone
102
+ - shorten or expand text
103
+ - remove or add emojis
104
+ - tweak style
105
+
106
+ When you respond:
107
+ - If the user clearly wants a simple answer, respond directly.
108
+ - If the user is asking to rewrite existing text (e.g. "shorten this",
109
+ "make it more professional", "remove emojis"), feel free to call tools
110
+ if they are available.
111
+ - Always return clean, user-ready copy (no JSON, no debug).
112
+ """.strip()
113
+
114
+
115
+
116
+ def _build_message_history(history_pairs: List[List[str]]) -> List[Message]:
117
+ """
118
+ Convert [[user, assistant], ...] into LangChain Human/AI messages.
119
+ """
120
+ messages: List[Message] = []
121
+ for pair in history_pairs:
122
+ if not pair or len(pair) != 2:
123
+ continue
124
+ user_text, assistant_text = pair
125
+ if user_text:
126
+ messages.append(HumanMessage(content=user_text))
127
+ if assistant_text:
128
+ messages.append(AIMessage(content=assistant_text))
129
+ return messages
130
+
131
+
132
+ def _get_tool_map(tools: List[BaseTool]) -> Dict[str, BaseTool]:
133
+ """
134
+ Convenience map: tool_name -> tool object.
135
+ """
136
+ return {tool.name: tool for tool in tools}
137
+
138
+
139
+ # --------------------------------------------------------------------
140
+ # Main agent entry point
141
+ # --------------------------------------------------------------------
142
+
143
+
144
+ def agent_chat_turn(
145
+ req: CopyRequest,
146
+ user_message: str,
147
+ history_pairs: List[List[str]] | None = None,
148
+ ) -> Tuple[str, str, list]:
149
+ ...
150
+ history_pairs = history_pairs or []
151
+
152
+ # 1) Build base messages: "system" prompt as a HumanMessage + history + new user
153
+ instructions = _build_system_prompt(req)
154
+
155
+ # IMPORTANT: use HumanMessage here, not SystemMessage
156
+ system_msg = HumanMessage(content=instructions)
157
+
158
+ history_msgs = _build_message_history(history_pairs)
159
+ new_user_msg = HumanMessage(content=user_message)
160
+
161
+ messages: List[Union[Message, ToolMessage]] = (
162
+ [system_msg] + history_msgs + [new_user_msg]
163
+ )
164
+
165
+
166
+ # 2) Prepare tools & model
167
+ tools: List[BaseTool] = get_rewrite_tools()
168
+ tool_map = _get_tool_map(tools)
169
+
170
+ llm = get_local_chat_model()
171
+ llm_with_tools = llm.bind_tools(tools)
172
+
173
+ # 3) First model call (decide whether to use tools)
174
+ ai_msg: AIMessage = llm_with_tools.invoke(messages)
175
+ raw_first = ai_msg.content or ""
176
+
177
+ # If the model does not request any tools, just return its answer
178
+ if not getattr(ai_msg, "tool_calls", None):
179
+ final_text = raw_first.strip()
180
+ return final_text, raw_first, []
181
+
182
+ # 4) Execute any requested tools
183
+ messages.append(ai_msg)
184
+ tool_messages: List[ToolMessage] = []
185
+
186
+ for tool_call in ai_msg.tool_calls:
187
+ tool_name = tool_call.get("name")
188
+ args = tool_call.get("args", {})
189
+ call_id = tool_call.get("id") or ""
190
+
191
+ tool = tool_map.get(tool_name)
192
+ if tool is None:
193
+ tool_output = f"Tool '{tool_name}' is not available."
194
+ else:
195
+ # LangChain tools usually implement .invoke()
196
+ try:
197
+ tool_output = tool.invoke(args)
198
+ except Exception as e:
199
+ tool_output = f"Tool '{tool_name}' failed with error: {e}"
200
+
201
+ tool_msg = ToolMessage(
202
+ content=str(tool_output),
203
+ tool_call_id=call_id,
204
+ )
205
+ tool_messages.append(tool_msg)
206
+
207
+ messages.extend(tool_messages)
208
+
209
+ # 5) Second model call: let the LLM see tool results and answer
210
+ final_ai: AIMessage = llm_with_tools.invoke(messages)
211
+ final_text = (final_ai.content or "").strip()
212
+ raw_second = final_ai.content or ""
213
+
214
+ audit: list = [] # reserved for tool call logs if you want later
215
+
216
+ return final_text, raw_second, audit
core_logic/chat_chain.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LangChain-based chat helper for copy generation.
3
+
4
+ We use:
5
+ - PromptTemplate from langchain_core
6
+ - ChatHuggingFace model from llm_config
7
+ - Simple chat history from the Gradio Chatbot (list of [user, assistant] pairs)
8
+
9
+ We DO NOT use SystemMessage, because the current model's chat template
10
+ does not support a "system" role. Instead, we fold all instructions and
11
+ campaign context into a single HumanMessage prompt, including platform
12
+ style guidelines (Phase 3).
13
+ """
14
+
15
+ from typing import List, Tuple
16
+
17
+ from langchain_core.prompts import PromptTemplate
18
+ from langchain_core.messages import HumanMessage
19
+
20
+ from core_logic.llm_config import get_local_chat_model
21
+ from .copy_pipeline import CopyRequest
22
+ from helpers.platform_rules import (
23
+ PLATFORM_RULES,
24
+ DEFAULT_PLATFORM_NAME,
25
+ PlatformConfig,
26
+ get_platform_style,
27
+ )
28
+ from helpers.validators import validate_and_edit
29
+
30
+
31
+ def _get_platform_config(name: str) -> PlatformConfig:
32
+ if name in PLATFORM_RULES:
33
+ return PLATFORM_RULES[name]
34
+ return PLATFORM_RULES[DEFAULT_PLATFORM_NAME]
35
+
36
+
37
+ def build_chat_prompt_template() -> PromptTemplate:
38
+ """
39
+ Template takes:
40
+ - brand, product, audience, goal, platform, tone, cta_style, extra_context
41
+ - char_cap
42
+ - style_voice, style_emoji_guideline, style_hashtag_guideline, style_length_guideline
43
+ - history (chat transcript as text)
44
+ - input (latest user message)
45
+ """
46
+ template = """
47
+ You are an expert social media marketer.
48
+ You help refine and iterate on social media posts for {platform}.
49
+
50
+ Campaign context:
51
+ - Brand: {brand}
52
+ - Product/Offer: {product}
53
+ - Target audience: {audience}
54
+ - Campaign goal: {goal}
55
+ - Tone requested by user: {tone}
56
+ - Call-to-action style: {cta_style}
57
+ - Extra context from the user: {extra_context}
58
+
59
+ Platform style guidelines for {platform}:
60
+ - Voice and personality: {style_voice}
61
+ - Emojis: {style_emoji_guideline}
62
+ - Hashtags: {style_hashtag_guideline}
63
+ - Length: {style_length_guideline}
64
+ - Character limit: approximately {char_cap} characters.
65
+
66
+ Here is the conversation so far between you and the user
67
+ about this campaign:
68
+
69
+ {history}
70
+
71
+ Now the user says:
72
+ {input}
73
+
74
+ Your task:
75
+ - Follow the platform style guidelines and tone.
76
+ - Respect the character limit as much as reasonably possible.
77
+ - If the user asks to edit or adapt an existing post, transform it accordingly.
78
+ - Do NOT include explanations, analysis, or labels in your answer.
79
+
80
+ Respond with ONLY the post text or edited post text
81
+ the user asked for. Do not add any extra commentary.
82
+ """
83
+ return PromptTemplate(
84
+ input_variables=[
85
+ "brand",
86
+ "product",
87
+ "audience",
88
+ "goal",
89
+ "platform",
90
+ "tone",
91
+ "cta_style",
92
+ "extra_context",
93
+ "char_cap",
94
+ "style_voice",
95
+ "style_emoji_guideline",
96
+ "style_hashtag_guideline",
97
+ "style_length_guideline",
98
+ "history",
99
+ "input",
100
+ ],
101
+ template=template.strip(),
102
+ )
103
+
104
+
105
+ def _format_history(history_pairs: List[Tuple[str, str]]) -> str:
106
+ """
107
+ Convert list of (user, assistant) messages into a simple text transcript.
108
+ """
109
+ if not history_pairs:
110
+ return "(No previous conversation yet.)"
111
+
112
+ lines = []
113
+ for u, a in history_pairs:
114
+ if u:
115
+ lines.append(f"User: {u}")
116
+ if a:
117
+ lines.append(f"Assistant: {a}")
118
+ return "\n".join(lines)
119
+
120
+
121
+ def chat_turn(
122
+ req: CopyRequest,
123
+ user_message: str,
124
+ history_pairs: List[Tuple[str, str]],
125
+ ):
126
+ """
127
+ Run one chat turn:
128
+
129
+ - Uses LangChain PromptTemplate + ChatHuggingFace (via get_local_chat_model)
130
+ - Uses history_pairs (from Gradio Chatbot) as conversation history
131
+ - Applies platform style guidelines (Phase 3)
132
+ - Applies validators (banned terms, char caps, etc.)
133
+ - Returns final_text, raw_text, audit
134
+ """
135
+ platform_cfg = _get_platform_config(req.platform_name)
136
+ style = get_platform_style(req.platform_name)
137
+
138
+ prompt_tmpl = build_chat_prompt_template()
139
+ history_text = _format_history(history_pairs)
140
+
141
+ # Build the full prompt string with context + style + history + latest user message
142
+ prompt_str = prompt_tmpl.format(
143
+ brand=req.brand or "",
144
+ product=req.product or "",
145
+ audience=req.audience or "",
146
+ goal=req.goal or "",
147
+ platform=style.get("name", req.platform_name or "Unknown platform"),
148
+ tone=req.tone or "friendly",
149
+ cta_style=req.cta_style or "soft",
150
+ extra_context=req.extra_context or "",
151
+ char_cap=str(platform_cfg.cap)
152
+ if hasattr(platform_cfg, "cap")
153
+ else str(getattr(platform_cfg, "char_cap", 280)),
154
+ style_voice=style.get("voice", ""),
155
+ style_emoji_guideline=style.get("emoji_guideline", ""),
156
+ style_hashtag_guideline=style.get("hashtag_guideline", ""),
157
+ style_length_guideline=style.get("length_guideline", ""),
158
+ history=history_text,
159
+ input=user_message,
160
+ )
161
+
162
+ # Call the ChatHuggingFace model with a single HumanMessage
163
+ chat_model = get_local_chat_model()
164
+ ai_msg = chat_model.invoke([HumanMessage(content=prompt_str)])
165
+ raw_text = ai_msg.content
166
+
167
+ # Apply your existing validators (banned phrases, length, etc.)
168
+ final_text, audit = validate_and_edit(raw_text, platform_cfg)
169
+
170
+ return final_text, raw_text, audit
core_logic/copy_pipeline.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Copy generation pipeline for Marketeer.
3
+
4
+ This wraps the low-level LLM client and the helper utilities
5
+ into a single `generate_copy` function that other parts of
6
+ the app (like the Gradio UI) can call.
7
+
8
+ High level:
9
+ - Build a structured prompt using the provided context.
10
+ - Call the LLM to generate text.
11
+ - Run validators (banned terms, length caps, etc.).
12
+ - Return raw text, final text, and an audit log.
13
+ """
14
+
15
+ from dataclasses import dataclass
16
+ from typing import Dict, Any, List, Tuple
17
+
18
+ from .llm_client import generate_text
19
+ from helpers.platform_rules import PLATFORM_RULES, DEFAULT_PLATFORM_NAME, PlatformConfig
20
+ from helpers.validators import validate_and_edit
21
+
22
+
23
+ @dataclass
24
+ class CopyRequest:
25
+ brand: str
26
+ product: str
27
+ audience: str
28
+ goal: str
29
+ platform_name: str
30
+ tone: str
31
+ cta_style: str
32
+ extra_context: str = ""
33
+
34
+
35
+ @dataclass
36
+ class CopyResponse:
37
+ platform: str
38
+ raw: str
39
+ final: str
40
+ cap: int
41
+ audit: List[Dict[str, Any]]
42
+
43
+
44
+ def _get_platform_config(name: str) -> PlatformConfig:
45
+ """Return a known PlatformConfig or default to Instagram."""
46
+ if name in PLATFORM_RULES:
47
+ return PLATFORM_RULES[name]
48
+ # allow simple aliases like "X" or "Twitter/X" later if you want
49
+ return PLATFORM_RULES[DEFAULT_PLATFORM_NAME]
50
+
51
+
52
+ def _build_prompt(req: CopyRequest, platform: PlatformConfig) -> str:
53
+ """
54
+ Build a reasonably structured prompt for the LLM.
55
+
56
+ This is intentionally simple for now; you can make it
57
+ fancier later (add examples, formatting, etc.).
58
+ """
59
+
60
+ lines = [
61
+ f"You are an expert social media marketer.",
62
+ f"Write a single post for {platform.name}.",
63
+ "",
64
+ f"Brand: {req.brand}",
65
+ f"Product/Offer: {req.product}",
66
+ f"Target audience: {req.audience}",
67
+ f"Campaign goal: {req.goal}",
68
+ f"Tone: {req.tone}",
69
+ f"Call-to-action style: {req.cta_style}",
70
+ ]
71
+
72
+ if req.extra_context.strip():
73
+ lines.append(f"Extra context: {req.extra_context.strip()}")
74
+
75
+ lines.append("")
76
+ lines.append(
77
+ f"Keep the copy within approximately {platform.char_cap} characters, "
78
+ f"and make it engaging but natural."
79
+ )
80
+ lines.append("Do not include explanations, just the post text itself.")
81
+
82
+ return "\n".join(lines)
83
+
84
+
85
+ def generate_copy(req: CopyRequest) -> CopyResponse:
86
+ """
87
+ Main entry point for marketing copy generation.
88
+
89
+ 1) Resolve platform config.
90
+ 2) Build a prompt.
91
+ 3) Call the LLM.
92
+ 4) Run validators and collect audit.
93
+ 5) Return structured response.
94
+ """
95
+ platform = _get_platform_config(req.platform_name)
96
+
97
+ prompt = _build_prompt(req, platform)
98
+
99
+ raw_text = generate_text(
100
+ prompt=prompt,
101
+ max_new_tokens=256,
102
+ temperature=0.8,
103
+ top_p=0.9,
104
+ )
105
+
106
+ final_text, audit = validate_and_edit(raw_text, platform)
107
+
108
+ return CopyResponse(
109
+ platform=platform.name,
110
+ raw=raw_text,
111
+ final=final_text,
112
+ cap=platform.char_cap,
113
+ audit=audit,
114
+ )
core_logic/langchain_llm.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LangChain-compatible LLM wrapper around our existing generate_text().
3
+
4
+ This lets us use LangChain chains, prompts, and memory
5
+ without changing the underlying HF model logic.
6
+ """
7
+
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from langchain_core.language_models.llms import LLM
11
+
12
+ from .llm_client import generate_text
13
+
14
+
15
+ class MarketeerLLM(LLM):
16
+ """
17
+ Minimal LangChain LLM that just calls our generate_text() helper.
18
+ """
19
+
20
+ def _call(
21
+ self,
22
+ prompt: str,
23
+ stop: Optional[List[str]] = None,
24
+ **kwargs: Any,
25
+ ) -> str:
26
+ # You can pass temp/top_p via kwargs if you want, or keep fixed config.
27
+ text = generate_text(
28
+ prompt=prompt,
29
+ max_new_tokens=kwargs.get("max_new_tokens", 256),
30
+ temperature=kwargs.get("temperature", 0.8),
31
+ top_p=kwargs.get("top_p", 0.9),
32
+ )
33
+
34
+ # Apply stop tokens if provided
35
+ if stop:
36
+ for s in stop:
37
+ if s in text:
38
+ text = text.split(s)[0]
39
+ break
40
+
41
+ return text.strip()
42
+
43
+ @property
44
+ def _llm_type(self) -> str:
45
+ return "marketeer_llm"
core_logic/llm_client.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM client for Marketeer.
3
+
4
+ This module exposes a single function:
5
+
6
+ generate_text(prompt: str, max_new_tokens: int = 256, temperature: float = 0.8, top_p: float = 0.9) -> str
7
+
8
+ Internally it:
9
+ - Loads the tokenizer & model once.
10
+ - Uses MODEL_ID from environment (or a sensible default).
11
+ - Lets `device_map="auto"` handle GPU/CPU placement when CUDA is available.
12
+ """
13
+
14
+ import os
15
+ from typing import Optional
16
+
17
+ import torch
18
+ from transformers import AutoTokenizer, AutoModelForCausalLM
19
+
20
+
21
+ # ----- Configuration -----
22
+
23
+ DEFAULT_MODEL_ID = "google/gemma-2-2b-it"
24
+ _MODEL_ID = os.getenv("MODEL_ID", DEFAULT_MODEL_ID)
25
+
26
+ _tokenizer: Optional[AutoTokenizer] = None
27
+ _model: Optional[AutoModelForCausalLM] = None
28
+
29
+
30
+ def _load_model_if_needed():
31
+ """Lazy-load tokenizer and model into global variables."""
32
+ global _tokenizer, _model
33
+
34
+ if _tokenizer is not None and _model is not None:
35
+ return
36
+
37
+ has_cuda = torch.cuda.is_available()
38
+
39
+ # bfloat16/float16 on GPU, float32 on CPU
40
+ if has_cuda:
41
+ dtype = torch.bfloat16
42
+ device_map = "auto" # let accelerate handle offload across GPU/CPU
43
+ else:
44
+ dtype = torch.float32
45
+ device_map = None
46
+
47
+ _tokenizer = AutoTokenizer.from_pretrained(_MODEL_ID)
48
+
49
+ _model = AutoModelForCausalLM.from_pretrained(
50
+ _MODEL_ID,
51
+ dtype=dtype, # use dtype instead of deprecated torch_dtype
52
+ device_map=device_map,
53
+ )
54
+
55
+ # Ensure pad token exists (some causal models don't define it)
56
+ if _tokenizer.pad_token is None:
57
+ _tokenizer.pad_token = _tokenizer.eos_token
58
+
59
+ _model.eval() # IMPORTANT: no _model.to(...) here
60
+
61
+
62
+ def generate_text(
63
+ prompt: str,
64
+ max_new_tokens: int = 256,
65
+ temperature: float = 0.8,
66
+ top_p: float = 0.9,
67
+ ) -> str:
68
+ """
69
+ Generate text from the model given a plain prompt.
70
+
71
+ Args:
72
+ prompt: The input text prompt.
73
+ max_new_tokens: Maximum number of new tokens to generate.
74
+ temperature: Sampling temperature (>1 = more random, <1 = more focused).
75
+ top_p: Nucleus sampling probability mass.
76
+
77
+ Returns:
78
+ The generated text (prompt excluded where possible).
79
+ """
80
+ if not isinstance(prompt, str):
81
+ raise TypeError("prompt must be a string")
82
+
83
+ cleaned_prompt = prompt.strip()
84
+ if not cleaned_prompt:
85
+ raise ValueError("prompt is empty after stripping whitespace")
86
+
87
+ _load_model_if_needed()
88
+ assert _tokenizer is not None
89
+ assert _model is not None
90
+
91
+ # DO NOT .to(device) here; accelerate handles device placement for us
92
+ inputs = _tokenizer(
93
+ cleaned_prompt,
94
+ return_tensors="pt",
95
+ )
96
+
97
+ with torch.no_grad():
98
+ output_ids = _model.generate(
99
+ **inputs,
100
+ max_new_tokens=max_new_tokens,
101
+ do_sample=True,
102
+ temperature=temperature,
103
+ top_p=top_p,
104
+ pad_token_id=_tokenizer.pad_token_id,
105
+ eos_token_id=_tokenizer.eos_token_id,
106
+ )
107
+
108
+ full_text = _tokenizer.decode(
109
+ output_ids[0],
110
+ skip_special_tokens=True,
111
+ clean_up_tokenization_spaces=True,
112
+ )
113
+
114
+ # Strip echoed prompt if present
115
+ if full_text.startswith(cleaned_prompt):
116
+ generated = full_text[len(cleaned_prompt):].lstrip()
117
+ else:
118
+ generated = full_text
119
+
120
+ return generated.strip()
core_logic/llm_config.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM configuration for Marketeer.
3
+
4
+ This module exposes helpers to get a LangChain ChatHuggingFace model,
5
+ backed by a local Hugging Face pipeline (for development).
6
+
7
+ Later, we can add a get_endpoint_chat_model() for HF Inference API.
8
+ """
9
+
10
+ from functools import lru_cache
11
+
12
+ from langchain_huggingface import ChatHuggingFace, HuggingFacePipeline
13
+ from transformers import BitsAndBytesConfig
14
+
15
+
16
+ MODEL_ID = "google/gemma-2-2b-it" # <-- change if you're using a different repo
17
+
18
+
19
+ @lru_cache(maxsize=1)
20
+ def get_local_chat_model() -> ChatHuggingFace:
21
+ """
22
+ Return a singleton ChatHuggingFace model running locally via transformers pipeline.
23
+
24
+ Uses 4-bit quantization to fit comfortably on a 6GB RTX 3060.
25
+ """
26
+ quant_config = BitsAndBytesConfig(
27
+ load_in_4bit=True,
28
+ bnb_4bit_quant_type="nf4",
29
+ bnb_4bit_compute_dtype="float16",
30
+ bnb_4bit_use_double_quant=True,
31
+ )
32
+
33
+ # HuggingFacePipeline wraps a transformers.pipeline under the hood
34
+ base_llm = HuggingFacePipeline.from_model_id(
35
+ model_id=MODEL_ID,
36
+ task="text-generation",
37
+ pipeline_kwargs=dict(
38
+ max_new_tokens=256,
39
+ do_sample=True,
40
+ temperature=0.8,
41
+ top_p=0.9,
42
+ return_full_text=False, # we only want the generated continuation
43
+ ),
44
+ model_kwargs={
45
+ "quantization_config": quant_config,
46
+ },
47
+ )
48
+
49
+ chat_model = ChatHuggingFace(llm=base_llm)
50
+ return chat_model
core_logic/rewrite_tools.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from langchain_core.tools import tool
3
+ from langchain_core.tools import BaseTool
4
+
5
+
6
+ @tool
7
+ def shorten_copy(text: str, max_words: int = 40) -> str:
8
+ """Shorten the given marketing copy while preserving core meaning and CTA."""
9
+ # simple baseline implementation; the LLM will often rewrite again
10
+ words = text.split()
11
+ if len(words) <= max_words:
12
+ return text
13
+ return " ".join(words[:max_words]) + "..."
14
+
15
+
16
+ @tool
17
+ def remove_emojis(text: str) -> str:
18
+ """Remove emojis and overly playful styling from the copy."""
19
+ # naive implementation – works fine as a starting point
20
+ import re
21
+
22
+ emoji_pattern = re.compile(
23
+ "["
24
+ "\U0001F600-\U0001F64F"
25
+ "\U0001F300-\U0001F5FF"
26
+ "\U0001F680-\U0001F6FF"
27
+ "\U0001F1E0-\U0001F1FF"
28
+ "]+",
29
+ flags=re.UNICODE,
30
+ )
31
+ no_emoji = emoji_pattern.sub("", text)
32
+ return " ".join(no_emoji.split())
33
+
34
+
35
+ def get_rewrite_tools() -> List[BaseTool]:
36
+ """
37
+ Return the list of tools the agent can use.
38
+ Add tone_shift, expand, etc. here over time.
39
+ """
40
+ return [shorten_copy, remove_emojis]
core_logic/video_pipeline.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core_logic/video_pipeline.py
2
+
3
+ """
4
+ Video script generation pipeline for Marketeer.
5
+
6
+ Phase 5: Use a structured Pydantic schema (VideoScriptResponse)
7
+ while keeping the external behaviour compatible with the existing UI.
8
+
9
+ High-level flow:
10
+ 1. Build a simple beat plan based on blueprint + duration.
11
+ 2. For each beat, ask the LLM for a JSON block with:
12
+ - voiceover
13
+ - on_screen
14
+ - shots
15
+ - broll
16
+ - captions
17
+ 3. Parse JSON into VideoBeat models.
18
+ 4. Return a VideoScriptResponse (plan + warnings).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ from dataclasses import dataclass
25
+ from typing import List, Dict, Any
26
+
27
+ from core_logic.llm_client import generate_text
28
+ from core_logic.video_schema import (
29
+ VideoBeat,
30
+ VideoPlan,
31
+ VideoScriptResponse,
32
+ )
33
+
34
+
35
+ # --------------------------------------------------------------------
36
+ # Request object coming from UI
37
+ # --------------------------------------------------------------------
38
+
39
+
40
+ @dataclass
41
+ class VideoRequest:
42
+ brand: str
43
+ product: str
44
+ audience: str
45
+ goal: str
46
+ blueprint_name: str
47
+ duration_sec: int
48
+ platform_name: str
49
+ style: str
50
+ extra_context: str = ""
51
+
52
+
53
+ # --------------------------------------------------------------------
54
+ # Internal helpers: plan building & prompting
55
+ # --------------------------------------------------------------------
56
+
57
+
58
+ def _build_basic_plan(req: VideoRequest) -> VideoPlan:
59
+ """
60
+ Build a very simple beat plan based on blueprint and duration.
61
+
62
+ Right now we keep this deterministic and lightweight.
63
+ Later, you can make this itself LLM-driven if you want.
64
+ """
65
+ total = max(req.duration_sec, 5)
66
+ blueprint = (req.blueprint_name or "short_ad").lower()
67
+
68
+ if blueprint == "short_ad":
69
+ # 3-beat: Hook -> Product -> CTA
70
+ beats_meta = [
71
+ ("Hook / Problem", "Hook viewer, show the pain or context.", 0.0, total * 0.33),
72
+ ("Product Moment", "Show product solving the problem.", total * 0.33, total * 0.66),
73
+ ("CTA / Finish", "Wrap up and clear CTA.", total * 0.66, total),
74
+ ]
75
+ elif blueprint == "ugc_review":
76
+ # 4-beat: Intro -> Problem -> Experience -> Recommendation
77
+ beats_meta = [
78
+ ("Intro / Self", "Introduce the speaker as a real user.", 0.0, total * 0.25),
79
+ ("Problem", "Describe the problem or frustration.", total * 0.25, total * 0.5),
80
+ ("Experience", "Explain how using the product felt / helped.", total * 0.5, total * 0.75),
81
+ ("Recommendation", "Recommend the product and invite viewer to try.", total * 0.75, total),
82
+ ]
83
+ else: # how_to or fallback
84
+ # 4-beat: Hook -> Step(s) -> Result -> CTA
85
+ beats_meta = [
86
+ ("Hook / Promise", "Hook viewer and promise what they will learn.", 0.0, total * 0.25),
87
+ ("Step-by-step (1)", "Show the first main step.", total * 0.25, total * 0.5),
88
+ ("Step-by-step (2)", "Show the second main step or refinement.", total * 0.5, total * 0.75),
89
+ ("Result / CTA", "Show final outcome and clear CTA.", total * 0.75, total),
90
+ ]
91
+
92
+ beats: List[VideoBeat] = []
93
+ for idx, (title, goal, t_start, t_end) in enumerate(beats_meta):
94
+ beats.append(
95
+ VideoBeat(
96
+ beat_index=idx,
97
+ title=title,
98
+ goal=goal,
99
+ t_start=float(round(t_start, 2)),
100
+ t_end=float(round(t_end, 2)),
101
+ voiceover="", # to be filled by LLM
102
+ on_screen="", # to be filled by LLM
103
+ shots=[],
104
+ broll=[],
105
+ captions=[],
106
+ )
107
+ )
108
+
109
+ plan = VideoPlan(
110
+ blueprint_name=req.blueprint_name,
111
+ duration_sec=total,
112
+ platform_name=req.platform_name,
113
+ style=req.style,
114
+ beats=beats,
115
+ )
116
+ return plan
117
+
118
+
119
+ def _build_beat_prompt(req: VideoRequest, plan: VideoPlan, beat: VideoBeat) -> str:
120
+ """
121
+ Build an instruction to generate **one beat** as a JSON object.
122
+ """
123
+ return f"""
124
+ You are helping create a short-form marketing video script.
125
+
126
+ Brand: {req.brand}
127
+ Product: {req.product}
128
+ Audience: {req.audience}
129
+ Campaign goal: {req.goal}
130
+ Platform: {req.platform_name}
131
+ Overall style: {req.style}
132
+ Extra context: {req.extra_context}
133
+
134
+ We are currently working on one beat of the video:
135
+
136
+ - Blueprint: {plan.blueprint_name}
137
+ - Beat index: {beat.beat_index}
138
+ - Beat title: {beat.title}
139
+ - Beat goal: {beat.goal}
140
+ - Start time: {beat.t_start} seconds
141
+ - End time: {beat.t_end} seconds
142
+
143
+ Return **only** a JSON object (no markdown, no backticks) with this shape:
144
+
145
+ {{
146
+ "voiceover": "string, the spoken line(s) for this beat",
147
+ "on_screen": "string, short text shown on screen",
148
+ "shots": ["list of camera shot ideas, strings"],
149
+ "broll": ["optional list of B-roll ideas, strings"],
150
+ "captions": ["optional list of caption lines, strings"]
151
+ }}
152
+
153
+ The voiceover should match the platform and style, and help achieve the beat goal.
154
+ Keep it concise but vivid.
155
+ """.strip()
156
+
157
+
158
+ def _extract_json_from_response(raw: str) -> Dict[str, Any]:
159
+ """
160
+ Try to extract a JSON object from the LLM response.
161
+
162
+ If it's already plain JSON, parse that.
163
+ If it's inside a markdown ```json block, extract the inner part.
164
+ Raises ValueError if parsing fails.
165
+ """
166
+ text = raw.strip()
167
+
168
+ # Common case: LLM wraps in ```json ... ```
169
+ if "```" in text:
170
+ # Take the content between the first pair of ``` blocks
171
+ parts = text.split("```")
172
+ # Expected pattern: ["", "json\\n{...}", ""]
173
+ if len(parts) >= 3:
174
+ candidate = parts[1]
175
+ # Strip a leading "json" or "JSON" line
176
+ candidate = candidate.lstrip().split("\n", 1)
177
+ if len(candidate) == 2 and candidate[0].lower() in ("json", "json:"):
178
+ text = candidate[1].strip()
179
+ else:
180
+ text = "\n".join(candidate).strip()
181
+
182
+ return json.loads(text)
183
+
184
+
185
+ # --------------------------------------------------------------------
186
+ # Public API: generate_video_script
187
+ # --------------------------------------------------------------------
188
+
189
+
190
+ def generate_video_script(
191
+ req: VideoRequest,
192
+ debug_first: bool = False,
193
+ ) -> VideoScriptResponse:
194
+ """
195
+ Main entry point used by the UI.
196
+
197
+ Generates a structured VideoScriptResponse (plan + warnings). The
198
+ UI can still access:
199
+ resp.plan
200
+ resp.beats (alias for resp.plan.beats)
201
+ resp.warnings
202
+ """
203
+ plan = _build_basic_plan(req)
204
+ warnings: List[str] = []
205
+ beats_out: List[VideoBeat] = []
206
+
207
+ for idx, beat in enumerate(plan.beats):
208
+ prompt = _build_beat_prompt(req, plan, beat)
209
+
210
+ raw = generate_text(
211
+ prompt=prompt,
212
+ max_new_tokens=256,
213
+ temperature=0.7,
214
+ top_p=0.9,
215
+ )
216
+
217
+ if debug_first and idx == 0:
218
+ print("=== RAW FIRST BEAT RESPONSE ===")
219
+ print(raw)
220
+ print("=" * 32)
221
+
222
+ try:
223
+ data = _extract_json_from_response(raw)
224
+ # Merge structured info into the beat
225
+ beat_updated = VideoBeat(
226
+ beat_index=beat.beat_index,
227
+ title=beat.title,
228
+ goal=beat.goal,
229
+ t_start=beat.t_start,
230
+ t_end=beat.t_end,
231
+ voiceover=str(data.get("voiceover", "")).strip(),
232
+ on_screen=str(data.get("on_screen", "")).strip(),
233
+ shots=list(data.get("shots", []) or []),
234
+ broll=list(data.get("broll", []) or []),
235
+ captions=list(data.get("captions", []) or []),
236
+ )
237
+ beats_out.append(beat_updated)
238
+ except Exception as e:
239
+ warnings.append(
240
+ f"Beat {beat.beat_index}: failed to parse JSON from model response ({e})."
241
+ )
242
+ # Fallback: keep the original beat with generic placeholders
243
+ beats_out.append(
244
+ VideoBeat(
245
+ beat_index=beat.beat_index,
246
+ title=beat.title,
247
+ goal=beat.goal,
248
+ t_start=beat.t_start,
249
+ t_end=beat.t_end,
250
+ voiceover="",
251
+ on_screen="",
252
+ shots=[],
253
+ broll=[],
254
+ captions=[],
255
+ )
256
+ )
257
+
258
+ # Construct final structured response
259
+ final_plan = VideoPlan(
260
+ blueprint_name=plan.blueprint_name,
261
+ duration_sec=plan.duration_sec,
262
+ platform_name=plan.platform_name,
263
+ style=plan.style,
264
+ beats=beats_out,
265
+ )
266
+
267
+ resp = VideoScriptResponse(
268
+ plan=final_plan,
269
+ warnings=warnings,
270
+ )
271
+ return resp
core_logic/video_schema.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core_logic/video_schema.py
2
+
3
+ from typing import List, Optional
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class VideoBeat(BaseModel):
8
+ """Single beat in the short-form video script."""
9
+ beat_index: int = Field(
10
+ ...,
11
+ description="Zero-based index of this beat in the sequence.",
12
+ )
13
+ title: str = Field(
14
+ ...,
15
+ description="Short title / label for this beat (e.g., 'Hook', 'Product Close-up').",
16
+ )
17
+ goal: str = Field(
18
+ ...,
19
+ description="What this beat is trying to achieve (hook, social proof, CTA, etc.).",
20
+ )
21
+ t_start: float = Field(
22
+ ...,
23
+ description="Approximate start time in seconds from the beginning of the video.",
24
+ )
25
+ t_end: float = Field(
26
+ ...,
27
+ description="Approximate end time in seconds from the beginning of the video.",
28
+ )
29
+ voiceover: str = Field(
30
+ ...,
31
+ description="Suggested voiceover line(s) for this beat.",
32
+ )
33
+ on_screen: str = Field(
34
+ ...,
35
+ description="Short on-screen text / caption for this beat.",
36
+ )
37
+ shots: List[str] = Field(
38
+ default_factory=list,
39
+ description="List of camera shots / visuals in this beat.",
40
+ )
41
+ broll: List[str] = Field(
42
+ default_factory=list,
43
+ description="Optional B-roll ideas for this beat.",
44
+ )
45
+ captions: List[str] = Field(
46
+ default_factory=list,
47
+ description="Suggested caption lines or overlays.",
48
+ )
49
+
50
+
51
+ class VideoPlan(BaseModel):
52
+ """High-level plan for the entire video."""
53
+ blueprint_name: str = Field(
54
+ ...,
55
+ description="Name of the blueprint used (e.g., 'short_ad', 'ugc_review', 'how_to').",
56
+ )
57
+ duration_sec: int = Field(
58
+ ...,
59
+ description="Total target duration of the video in seconds.",
60
+ )
61
+ platform_name: str = Field(
62
+ ...,
63
+ description="Target platform label (e.g., 'Instagram Reels', 'YouTube Shorts').",
64
+ )
65
+ style: str = Field(
66
+ ...,
67
+ description="Overall style (e.g., 'warm and energetic').",
68
+ )
69
+ beats: List[VideoBeat] = Field(
70
+ default_factory=list,
71
+ description="List of beats that make up this video.",
72
+ )
73
+
74
+
75
+ class VideoScriptResponse(BaseModel):
76
+ """
77
+ Full structured response used by the app and UI.
78
+ """
79
+ plan: VideoPlan = Field(
80
+ ...,
81
+ description="High-level plan metadata and beat list.",
82
+ )
83
+ warnings: List[str] = Field(
84
+ default_factory=list,
85
+ description="Any warnings about parsing, timing, or beat structure.",
86
+ )
87
+
88
+ @property
89
+ def beats(self) -> List[VideoBeat]:
90
+ """
91
+ Backwards-compatible alias so older code can still do resp.beats.
92
+ Internally, beats live on resp.plan.beats.
93
+ """
94
+ return self.plan.beats
helpers/__init__.py ADDED
File without changes
helpers/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (151 Bytes). View file
 
helpers/__pycache__/blueprints.cpython-310.pyc ADDED
Binary file (3.21 kB). View file
 
helpers/__pycache__/json_utils.cpython-310.pyc ADDED
Binary file (1.93 kB). View file
 
helpers/__pycache__/platform_rules.cpython-310.pyc ADDED
Binary file (3.07 kB). View file
 
helpers/__pycache__/platform_styles.cpython-310.pyc ADDED
Binary file (3.2 kB). View file
 
helpers/__pycache__/validators.cpython-310.pyc ADDED
Binary file (1.88 kB). View file
 
helpers/blueprints.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video blueprints for Marketeer.
3
+
4
+ These define high-level structures (beats) for different
5
+ short-form video types like short ads, UGC reviews, and how-tos.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import List, Dict
10
+
11
+
12
+ @dataclass
13
+ class BeatTemplate:
14
+ id: str
15
+ title: str
16
+ goal: str
17
+ weight: float # relative share of total duration (sum of weights ≈ 1.0)
18
+
19
+
20
+ @dataclass
21
+ class Blueprint:
22
+ name: str
23
+ description: str
24
+ beats: List[BeatTemplate]
25
+
26
+
27
+ def _short_ad() -> Blueprint:
28
+ beats = [
29
+ BeatTemplate(
30
+ id="hook",
31
+ title="Hook",
32
+ goal="Grab attention in the first second and stop the scroll.",
33
+ weight=0.2,
34
+ ),
35
+ BeatTemplate(
36
+ id="problem",
37
+ title="Problem",
38
+ goal="Show the pain point the viewer feels right now.",
39
+ weight=0.2,
40
+ ),
41
+ BeatTemplate(
42
+ id="solution",
43
+ title="Solution",
44
+ goal="Introduce the product as the clear solution.",
45
+ weight=0.3,
46
+ ),
47
+ BeatTemplate(
48
+ id="proof",
49
+ title="Proof",
50
+ goal="Show quick proof: results, social proof, or credibility.",
51
+ weight=0.2,
52
+ ),
53
+ BeatTemplate(
54
+ id="cta",
55
+ title="Call to Action",
56
+ goal="Give a clear, simple next step.",
57
+ weight=0.1,
58
+ ),
59
+ ]
60
+ return Blueprint(
61
+ name="short_ad",
62
+ description="Punchy short ad for Reels/Shorts/TikTok with strong hook and CTA.",
63
+ beats=beats,
64
+ )
65
+
66
+
67
+ def _ugc_review() -> Blueprint:
68
+ beats = [
69
+ BeatTemplate(
70
+ id="intro",
71
+ title="UGC Intro",
72
+ goal="Introduce yourself quickly and mention the product.",
73
+ weight=0.2,
74
+ ),
75
+ BeatTemplate(
76
+ id="before",
77
+ title="Before",
78
+ goal="Describe life before using the product (the struggle).",
79
+ weight=0.25,
80
+ ),
81
+ BeatTemplate(
82
+ id="experience",
83
+ title="Experience",
84
+ goal="Describe what it was like actually trying the product.",
85
+ weight=0.3,
86
+ ),
87
+ BeatTemplate(
88
+ id="after",
89
+ title="After",
90
+ goal="Describe the positive results / outcome.",
91
+ weight=0.15,
92
+ ),
93
+ BeatTemplate(
94
+ id="recommend",
95
+ title="Recommendation & CTA",
96
+ goal="Recommend the product and give a simple prompt to act.",
97
+ weight=0.1,
98
+ ),
99
+ ]
100
+ return Blueprint(
101
+ name="ugc_review",
102
+ description="User-generated style review with before/after flow.",
103
+ beats=beats,
104
+ )
105
+
106
+
107
+ def _how_to() -> Blueprint:
108
+ beats = [
109
+ BeatTemplate(
110
+ id="intro",
111
+ title="Intro",
112
+ goal="Tell viewers what they will learn and why it matters.",
113
+ weight=0.2,
114
+ ),
115
+ BeatTemplate(
116
+ id="step1",
117
+ title="Step 1",
118
+ goal="Explain and demo the first key step.",
119
+ weight=0.25,
120
+ ),
121
+ BeatTemplate(
122
+ id="step2",
123
+ title="Step 2",
124
+ goal="Explain and demo the second key step.",
125
+ weight=0.25,
126
+ ),
127
+ BeatTemplate(
128
+ id="step3",
129
+ title="Step 3",
130
+ goal="Optional third step or bonus tip.",
131
+ weight=0.15,
132
+ ),
133
+ BeatTemplate(
134
+ id="wrap",
135
+ title="Recap & CTA",
136
+ goal="Recap key points and suggest the next action.",
137
+ weight=0.15,
138
+ ),
139
+ ]
140
+ return Blueprint(
141
+ name="how_to",
142
+ description="Educational explainer with clear steps and recap.",
143
+ beats=beats,
144
+ )
145
+
146
+
147
+ BLUEPRINTS: Dict[str, Blueprint] = {
148
+ "short_ad": _short_ad(),
149
+ "ugc_review": _ugc_review(),
150
+ "how_to": _how_to(),
151
+ }
152
+
153
+
154
+ DEFAULT_BLUEPRINT = "short_ad"
155
+
156
+
157
+ def get_blueprint(name: str) -> Blueprint:
158
+ """Return a known blueprint or the default one."""
159
+ if name in BLUEPRINTS:
160
+ return BLUEPRINTS[name]
161
+ return BLUEPRINTS[DEFAULT_BLUEPRINT]
helpers/json_utils.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ JSON extraction and fallback utilities for video scripting.
3
+
4
+ We try to pull a JSON object out of a model's response, and if
5
+ that fails, we return None so the caller can use a fallback block.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Dict, Optional
10
+
11
+
12
+ def extract_json_block(text: str) -> Optional[Dict[str, Any]]:
13
+ """
14
+ Try to parse a JSON object from the given text.
15
+
16
+ Strategy:
17
+ 1. Try json.loads on the whole string.
18
+ 2. If that fails, look for the first '{' and last '}' and parse that slice.
19
+ 3. If still failing, return None.
20
+ """
21
+ text = text.strip()
22
+ if not text:
23
+ return None
24
+
25
+ # 1) raw attempt
26
+ try:
27
+ obj = json.loads(text)
28
+ if isinstance(obj, dict):
29
+ return obj
30
+ except Exception:
31
+ pass
32
+
33
+ # 2) substring between first '{' and last '}'
34
+ start = text.find("{")
35
+ end = text.rfind("}")
36
+ if start == -1 or end == -1 or end <= start:
37
+ return None
38
+
39
+ candidate = text[start : end + 1]
40
+ try:
41
+ obj = json.loads(candidate)
42
+ if isinstance(obj, dict):
43
+ return obj
44
+ except Exception:
45
+ return None
46
+
47
+ return None
48
+
49
+
50
+ def fallback_block(beat_title: str) -> Dict[str, Any]:
51
+ """
52
+ Provide a safe default block when JSON parsing fails.
53
+
54
+ The strings here are intentionally generic; the model's real
55
+ responses should normally override these when JSON is valid.
56
+ """
57
+ return {
58
+ "voiceover": f"Introduce the idea for the '{beat_title}' part in a clear, simple line.",
59
+ "on_screen": f"{beat_title} on screen.",
60
+ "shots": [
61
+ f"Shot of the main subject related to {beat_title.lower()}.",
62
+ "Close-up shot for extra detail.",
63
+ "Wide shot to show context or environment.",
64
+ ],
65
+ "broll": [
66
+ "Supporting b-roll that reinforces the message.",
67
+ "Cutaway showing product or user in action.",
68
+ ],
69
+ "captions": [
70
+ f"{beat_title} caption text.",
71
+ ],
72
+ }
helpers/platform_rules.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Dict
3
+
4
+
5
+ # --- Core platform config (caps, hashtags, emojis) ---
6
+
7
+
8
+ @dataclass
9
+ class PlatformConfig:
10
+ """
11
+ Basic platform constraints used by the validator and pipelines.
12
+ """
13
+ name: str
14
+ char_cap: int
15
+ hashtags_max: int
16
+ emoji_max: int
17
+
18
+ @property
19
+ def cap(self) -> int:
20
+ """
21
+ Backwards-compatible alias for char_cap.
22
+ Some older code might reference .cap instead of .char_cap.
23
+ """
24
+ return self.char_cap
25
+
26
+
27
+ # Character caps and simple rules per platform.
28
+ PLATFORM_RULES: Dict[str, PlatformConfig] = {
29
+ "Instagram": PlatformConfig(
30
+ name="Instagram",
31
+ char_cap=2200,
32
+ hashtags_max=5,
33
+ emoji_max=5,
34
+ ),
35
+ "Facebook": PlatformConfig(
36
+ name="Facebook",
37
+ char_cap=125,
38
+ hashtags_max=0,
39
+ emoji_max=1,
40
+ ),
41
+ "LinkedIn": PlatformConfig(
42
+ name="LinkedIn",
43
+ char_cap=3000,
44
+ hashtags_max=3,
45
+ emoji_max=2,
46
+ ),
47
+ "Twitter": PlatformConfig(
48
+ name="Twitter",
49
+ char_cap=280,
50
+ hashtags_max=2,
51
+ emoji_max=2,
52
+ ),
53
+ }
54
+
55
+ DEFAULT_PLATFORM_NAME: str = "Instagram"
56
+
57
+
58
+ # --- Banned phrase map (for safer language) ---
59
+
60
+
61
+ # Regex patterns mapped to replacement phrases.
62
+ # The validator will use this to make copy less spammy / risky.
63
+ BANNED_MAP: Dict[str, str] = {
64
+ r"\bguarantee(d|s)?\b": "aim to",
65
+ r"\bno[-\s]?risk\b": "low risk",
66
+ # Add more patterns as needed
67
+ }
68
+
69
+
70
+ # --- Platform style profiles (Phase 3) ---
71
+
72
+
73
+ # Each entry describes how copy should "feel" on that platform.
74
+ # These are used at prompt level in chat_chain so the LLM
75
+ # clearly understands the expectations per platform.
76
+ PLATFORM_STYLES: Dict[str, Dict] = {
77
+ "Instagram": {
78
+ "name": "Instagram",
79
+ "voice": (
80
+ "fun, casual, and energetic. Speak like a friendly social media manager "
81
+ "talking to followers."
82
+ ),
83
+ "emoji_guideline": (
84
+ "Emojis are welcome. Use them to enhance the energy of the post, "
85
+ "but avoid clutter."
86
+ ),
87
+ "hashtag_guideline": (
88
+ "Use 3–5 relevant hashtags at the end of the post. "
89
+ "Hashtags should be short, readable, and on-topic."
90
+ ),
91
+ "length_guideline": "Short to medium length caption is ideal.",
92
+ },
93
+ "Facebook": {
94
+ "name": "Facebook",
95
+ "voice": (
96
+ "friendly and conversational, but slightly more explanatory than Instagram."
97
+ ),
98
+ "emoji_guideline": (
99
+ "Emojis are allowed, but use them sparingly for emphasis only."
100
+ ),
101
+ "hashtag_guideline": (
102
+ "One or two hashtags are okay, but they are optional. "
103
+ "Focus more on clear, readable text."
104
+ ),
105
+ "length_guideline": "Short to medium length post with a clear main message.",
106
+ },
107
+ "LinkedIn": {
108
+ "name": "LinkedIn",
109
+ "voice": (
110
+ "professional, clear, and value-focused. "
111
+ "Write like a marketer speaking to working professionals."
112
+ ),
113
+ "emoji_guideline": (
114
+ "Avoid or minimize emojis. If used at all, keep them professional and sparse."
115
+ ),
116
+ "hashtag_guideline": (
117
+ "1–3 relevant, professional hashtags are acceptable at the end. "
118
+ "Do not overuse hashtags."
119
+ ),
120
+ "length_guideline": (
121
+ "Short to medium length update. Prioritize clarity and professionalism."
122
+ ),
123
+ },
124
+ "Twitter": {
125
+ "name": "Twitter",
126
+ "voice": (
127
+ "short, punchy, and attention-grabbing. "
128
+ "Get to the point quickly."
129
+ ),
130
+ "emoji_guideline": (
131
+ "Emojis are fine but keep them minimal and highly relevant."
132
+ ),
133
+ "hashtag_guideline": (
134
+ "1–2 strong, relevant hashtags max. Avoid hashtag spam."
135
+ ),
136
+ "length_guideline": "Very concise. Every word should earn its place.",
137
+ },
138
+ }
139
+
140
+ DEFAULT_PLATFORM_STYLE: Dict = PLATFORM_STYLES.get("Instagram")
141
+
142
+
143
+ def get_platform_style(name: str) -> Dict:
144
+ """
145
+ Return a style profile dict for a given platform name.
146
+
147
+ If the platform is unknown, fall back to DEFAULT_PLATFORM_STYLE.
148
+ """
149
+ return PLATFORM_STYLES.get(name, DEFAULT_PLATFORM_STYLE)
helpers/platform_styles.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Platform personality profiles for Marketeer.
3
+
4
+ These capture HOW each platform prefers to communicate:
5
+ - voice & tone
6
+ - emoji usage
7
+ - hashtag style
8
+ - formatting preferences
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Dict
13
+
14
+
15
+ @dataclass
16
+ class PlatformStyle:
17
+ name: str
18
+ voice: str
19
+ emoji_guideline: str
20
+ hashtag_guideline: str
21
+ formatting_guideline: str
22
+ extra_notes: str = ""
23
+
24
+
25
+ # Core style definitions
26
+ PLATFORM_STYLES: Dict[str, PlatformStyle] = {
27
+ "Instagram": PlatformStyle(
28
+ name="Instagram",
29
+ voice="Casual, energetic, playful. Focus on vibes, feelings, and moments.",
30
+ emoji_guideline=(
31
+ "Use emojis naturally to enhance mood (1–5 per post). "
32
+ "Avoid overloading every word with emojis."
33
+ ),
34
+ hashtag_guideline=(
35
+ "Use 3–5 relevant hashtags at the end of the post. "
36
+ "Mix branded and generic hashtags (e.g., #BrewBlissCafe, #WeekendVibes)."
37
+ ),
38
+ formatting_guideline=(
39
+ "Short paragraphs, line breaks for readability, occasional emphasis with ALL CAPS "
40
+ "or **bold style** (if supported)."
41
+ ),
42
+ extra_notes="Hook in the first line. Make it thumb-stopping.",
43
+ ),
44
+ "Facebook": PlatformStyle(
45
+ name="Facebook",
46
+ voice="Friendly and conversational, but a bit more explanatory than Instagram.",
47
+ emoji_guideline=(
48
+ "Use emojis sparingly (0–2 per post), mainly to highlight key ideas."
49
+ ),
50
+ hashtag_guideline=(
51
+ "Hashtags are optional. If used, limit to 1–2 relevant tags."
52
+ ),
53
+ formatting_guideline=(
54
+ "1–3 short paragraphs. Clear, readable, and easy to skim."
55
+ ),
56
+ extra_notes="Good place for slightly longer explanations or promotions.",
57
+ ),
58
+ "LinkedIn": PlatformStyle(
59
+ name="LinkedIn",
60
+ voice=(
61
+ "Professional, clear, and value-driven. Focus on benefits, outcomes, and credibility. "
62
+ "Write as if speaking to working professionals."
63
+ ),
64
+ emoji_guideline=(
65
+ "Avoid emojis in most cases. If absolutely necessary, limit to 0–1 subtle emoji."
66
+ ),
67
+ hashtag_guideline=(
68
+ "Use 0–3 professional hashtags at the end if needed (e.g., #Marketing, #CustomerExperience)."
69
+ ),
70
+ formatting_guideline=(
71
+ "Short, well-structured paragraphs. Avoid slang. No all-caps. "
72
+ "Sound confident and polished."
73
+ ),
74
+ extra_notes="Highlight business value, customer experience, and trust.",
75
+ ),
76
+ "Twitter": PlatformStyle(
77
+ name="Twitter",
78
+ voice="Short, punchy, and to the point. Witty if possible.",
79
+ emoji_guideline=(
80
+ "Use emojis sparingly (0–2) to add flavor, not clutter."
81
+ ),
82
+ hashtag_guideline=(
83
+ "Use 1–3 short hashtags. Prioritize relevance over quantity."
84
+ ),
85
+ formatting_guideline=(
86
+ "Single-paragraph or a short thread. Max impact in minimal characters."
87
+ ),
88
+ extra_notes="Lead with the core hook in the first few words.",
89
+ ),
90
+ # Fallback / generic style
91
+ "Generic": PlatformStyle(
92
+ name="Generic",
93
+ voice="Clear, friendly, and informative.",
94
+ emoji_guideline="Use emojis only if they genuinely add clarity or mood.",
95
+ hashtag_guideline="Use a small number of relevant hashtags if appropriate.",
96
+ formatting_guideline="Keep sentences and paragraphs easy to read.",
97
+ extra_notes="Adapt tone slightly based on the brand and audience.",
98
+ ),
99
+ }
100
+
101
+
102
+ DEFAULT_STYLE_NAME = "Generic"
103
+
104
+
105
+ def get_platform_style(name: str) -> PlatformStyle:
106
+ """
107
+ Return the platform style for the given name, falling back to Generic.
108
+ """
109
+ if name in PLATFORM_STYLES:
110
+ return PLATFORM_STYLES[name]
111
+ return PLATFORM_STYLES[DEFAULT_STYLE_NAME]
helpers/validators.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Validation and gentle editing layer for generated copy.
3
+
4
+ This module:
5
+ - Applies banned-term replacements (e.g., "guaranteed" -> "aim to").
6
+ - Trims text to platform character cap.
7
+ - Returns an audit log of what changed.
8
+ """
9
+
10
+ from typing import List, Dict, Tuple
11
+
12
+ from .platform_rules import PlatformConfig
13
+
14
+
15
+ # Soft language map: you can expand this list as you like
16
+ BANNED_MAP = {
17
+ "guaranteed": "aim to",
18
+ "guarantee": "aim to",
19
+ "no risk": "low risk",
20
+ }
21
+
22
+
23
+ def _apply_banned_terms(text: str) -> Tuple[str, List[Dict]]:
24
+ """Replace banned phrases and record changes."""
25
+ audit: List[Dict] = []
26
+ cleaned = text
27
+
28
+ for bad, replacement in BANNED_MAP.items():
29
+ if bad.lower() in cleaned.lower():
30
+ before = cleaned
31
+ # simple case-insensitive replace
32
+ cleaned = cleaned.replace(bad, replacement)
33
+ cleaned = cleaned.replace(bad.capitalize(), replacement)
34
+ cleaned = cleaned.replace(bad.upper(), replacement.upper())
35
+ audit.append(
36
+ {
37
+ "rule": "banned_term",
38
+ "bad": bad,
39
+ "replacement": replacement,
40
+ }
41
+ )
42
+
43
+ return cleaned, audit
44
+
45
+
46
+ def _apply_length_cap(text: str, platform: PlatformConfig) -> Tuple[str, List[Dict]]:
47
+ """Trim text to the platform's character cap if necessary."""
48
+ audit: List[Dict] = []
49
+ cap = platform.char_cap
50
+
51
+ if len(text) > cap:
52
+ before_len = len(text)
53
+ trimmed = text[:cap].rstrip()
54
+ audit.append(
55
+ {
56
+ "rule": "length_trim",
57
+ "before_len": before_len,
58
+ "after_len": len(trimmed),
59
+ "cap": cap,
60
+ }
61
+ )
62
+ return trimmed, audit
63
+
64
+ return text, audit
65
+
66
+
67
+ def validate_and_edit(
68
+ text: str,
69
+ platform: PlatformConfig,
70
+ ) -> Tuple[str, List[Dict]]:
71
+ """
72
+ Apply all validators in order and collect a combined audit log.
73
+
74
+ Returns:
75
+ final_text, audit_log
76
+ """
77
+ audit_log: List[Dict] = []
78
+
79
+ # 1) banned terms
80
+ text, banned_audit = _apply_banned_terms(text)
81
+ audit_log.extend(banned_audit)
82
+
83
+ # 2) length trim
84
+ text, trim_audit = _apply_length_cap(text, platform)
85
+ audit_log.extend(trim_audit)
86
+
87
+ # (you can add more steps later: CTA normalization, emoji limits, etc.)
88
+ return text, audit_log
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ torch
3
+ transformers
4
+ rich
5
+ sentence-transformers
6
+ textstat
7
+ regex
8
+ tiktoken
9
+ accelerate
10
+ ipywidgets
11
+ ipykernel
12
+ gradio
13
+ langchain
14
+ langchain-core
15
+ langchain-community
16
+ langchain-huggingface
17
+ bitsandbytes
test_llm_backend.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from core_logic.llm_config import get_local_chat_model
2
+ from langchain_core.messages import HumanMessage
3
+
4
+
5
+ def main():
6
+ # Load the ChatHuggingFace model (cached after first call)
7
+ chat_model = get_local_chat_model()
8
+
9
+ # Instead of SystemMessage, fold instructions into the human message
10
+ prompt = (
11
+ "You are a friendly marketing copywriter.\n\n"
12
+ "Write a short, fun one-line ad for a coffee shop."
13
+ )
14
+
15
+ messages = [
16
+ HumanMessage(content=prompt),
17
+ ]
18
+
19
+ # Call the model
20
+ ai_msg = chat_model.invoke(messages)
21
+
22
+ # Show the result
23
+ print("Response type:", type(ai_msg))
24
+ print("Response content:\n")
25
+ print(ai_msg.content)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
todo.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ 1. Merge the both screens, when clicked on generate copy , first it gives the result, then user mustt be able to provide the inuputs in the chat window.
2
+ 2. Campain goals can be dropdown, generally campaing goals are fixed like lead generation, increase awareness, drive website traffic etc.
3
+ 3. Insert feedback section, to receive inuts from the users who used the tool.
ui/__init__.py ADDED
File without changes
ui/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (146 Bytes). View file
 
ui/__pycache__/gradio_ui.cpython-310.pyc ADDED
Binary file (11.2 kB). View file
 
ui/gradio_ui.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio UI for Marketeer (copy + video script).
3
+
4
+ New UX:
5
+
6
+ - User fills the campaign form (brand, product, audience, goal, platform, tone, CTA, extra context).
7
+ - Clicking "Generate Copy" creates a FIRST DRAFT and shows it as an assistant
8
+ message in a single chat interface.
9
+ - The user then continues the conversation in that SAME chat window
10
+ (no separate one-shot box / separate draft box).
11
+ - Feedback is linked to the LAST assistant response via a simple
12
+ rating + comment section under the chat.
13
+ """
14
+
15
+ from typing import Any, Dict, List
16
+
17
+ import gradio as gr
18
+
19
+ from core_logic.copy_pipeline import CopyRequest, generate_copy
20
+ from core_logic.video_pipeline import VideoRequest, generate_video_script
21
+ # from core_logic.chat_chain import chat_turn
22
+ from core_logic.chat_agent import agent_chat_turn
23
+ from core_logic.copy_pipeline import CopyRequest
24
+
25
+
26
+
27
+
28
+ # ----- Small helpers -----
29
+
30
+
31
+ def _build_goal_text(goal_preset: str, goal_custom: str) -> str:
32
+ """
33
+ Combine preset and custom goal fields into one text.
34
+
35
+ Logic:
36
+ - If custom goal is provided, use that.
37
+ - Else, use the preset goal (dropdown).
38
+ - Else, empty string.
39
+ """
40
+ goal_custom = (goal_custom or "").strip()
41
+ goal_preset = (goal_preset or "").strip()
42
+ return goal_custom or goal_preset or ""
43
+
44
+
45
+ # ----- Backend wrapper functions for Gradio -----
46
+
47
+
48
+ def _generate_first_copy_ui(
49
+ brand: str,
50
+ product: str,
51
+ audience: str,
52
+ goal_preset: str,
53
+ goal_custom: str,
54
+ platform_name: str,
55
+ tone: str,
56
+ cta_style: str,
57
+ extra_context: str,
58
+ ):
59
+ """
60
+ First-step copy generation using the form fields.
61
+ The result is shown as the FIRST assistant message in the chat.
62
+
63
+ Returns:
64
+ - chat_history: list of [user, assistant] pairs for the Chatbot component
65
+ Here we start with a single assistant message containing the first draft.
66
+ """
67
+ goal_text = _build_goal_text(goal_preset, goal_custom)
68
+
69
+ req = CopyRequest(
70
+ brand=brand or "",
71
+ product=product or "",
72
+ audience=audience or "",
73
+ goal=goal_text or "",
74
+ platform_name=platform_name or "Instagram",
75
+ tone=tone or "friendly",
76
+ cta_style=cta_style or "soft",
77
+ extra_context=extra_context or "",
78
+ )
79
+
80
+ resp = generate_copy(req)
81
+
82
+ first_post = (resp.final or "").strip()
83
+ if not first_post:
84
+ first_post = "I tried to generate a post, but the result was empty. Please try again."
85
+
86
+ # Seed chat: one assistant message with the first draft
87
+ chat_history: List[List[str]] = [["", first_post]]
88
+
89
+ return chat_history
90
+
91
+
92
+ def _chat_copy_ui(
93
+ chat_history,
94
+ user_message: str,
95
+ brand: str,
96
+ product: str,
97
+ audience: str,
98
+ goal_preset: str,
99
+ goal_custom: str,
100
+ platform_name: str,
101
+ tone: str,
102
+ cta_style: str,
103
+ extra_context: str,
104
+ ):
105
+ """
106
+ Chat handler for the Copy tab using the advanced agent with tools.
107
+
108
+ Parameters must match the order of inputs in send_btn.click():
109
+
110
+ inputs=[
111
+ chatbox,
112
+ user_msg,
113
+ brand,
114
+ product,
115
+ audience,
116
+ goal_preset,
117
+ goal_custom,
118
+ platform_name,
119
+ tone,
120
+ cta_style,
121
+ extra_context,
122
+ ]
123
+
124
+ - Uses campaign context (brand, product, audience, goal, platform, tone, CTA)
125
+ - Uses chat_history (list of [user, assistant] pairs) as previous conversation
126
+ - Returns updated chat_history and clears the input box.
127
+ """
128
+ # If user_message is empty, just return the same state
129
+ if not user_message or not user_message.strip():
130
+ return chat_history, user_message
131
+
132
+ # Merge preset + custom goal into a single text
133
+ goal_text = _build_goal_text(goal_preset, goal_custom)
134
+
135
+ # Build the CopyRequest from the form fields
136
+ req = CopyRequest(
137
+ brand=brand or "",
138
+ product=product or "",
139
+ audience=audience or "",
140
+ goal=goal_text,
141
+ platform_name=platform_name or "Instagram",
142
+ tone=tone or "friendly",
143
+ cta_style=cta_style or "soft",
144
+ extra_context=extra_context or "",
145
+ )
146
+
147
+ # Gradio Chatbot history comes in as list of [user, assistant] pairs
148
+ history_pairs = chat_history or []
149
+
150
+ # Call our advanced agent (which can use rewrite tools internally)
151
+ final_text, raw_text, audit = agent_chat_turn(
152
+ req=req,
153
+ user_message=user_message,
154
+ history_pairs=history_pairs,
155
+ )
156
+
157
+ # Append the new turn to history
158
+ new_history = history_pairs + [[user_message, final_text]]
159
+
160
+ # Return updated history and clear the input box
161
+ return new_history, ""
162
+
163
+
164
+ def _clear_chat():
165
+ """
166
+ Clear chat history.
167
+ """
168
+ return []
169
+
170
+
171
+ def _submit_feedback_for_last_reply(
172
+ chat_history,
173
+ fb_rating: str,
174
+ fb_text: str,
175
+ brand: str,
176
+ platform_name: str,
177
+ goal_preset: str,
178
+ goal_custom: str,
179
+ ):
180
+ """
181
+ Feedback handler tied to the LAST assistant message in the chat.
182
+
183
+ We log:
184
+ - Brand, Platform, Goal
185
+ - Rating (e.g., 👍 / 👎)
186
+ - Free-text feedback
187
+ - The last assistant reply text
188
+
189
+ and return a short status message.
190
+ """
191
+ if not chat_history:
192
+ return "No messages yet. Generate a post or chat first, then leave feedback."
193
+
194
+ # chat_history is a list of [user, assistant] pairs.
195
+ # The last pair's assistant message is the one we care about.
196
+ last_user, last_assistant = chat_history[-1]
197
+ last_assistant = last_assistant or "(empty reply)"
198
+
199
+ fb_rating = fb_rating or "(not provided)"
200
+ fb_text = fb_text or "(no comment)"
201
+ brand = brand or "(not provided)"
202
+ platform_name = platform_name or "(not provided)"
203
+
204
+ goal_text = _build_goal_text(goal_preset, goal_custom)
205
+ goal_text = goal_text or "(not provided)"
206
+
207
+ print("=== MARKETEER FEEDBACK (last reply) ===")
208
+ print(f"Brand: {brand}")
209
+ print(f"Platform: {platform_name}")
210
+ print(f"Goal: {goal_text}")
211
+ print(f"Rating: {fb_rating}")
212
+ print("User feedback text:")
213
+ print(fb_text)
214
+ print("--- Last assistant reply ---")
215
+ print(last_assistant)
216
+ print("=======================================")
217
+
218
+ return "✅ Thanks for your feedback on the last reply!"
219
+
220
+
221
+ def _generate_video_ui(
222
+ brand: str,
223
+ product: str,
224
+ audience: str,
225
+ goal: str,
226
+ blueprint_name: str,
227
+ duration_sec: int,
228
+ platform_name: str,
229
+ style: str,
230
+ extra_context: str,
231
+ debug_first: bool,
232
+ ) -> Dict[str, Any]:
233
+ """
234
+ Wrapper around generate_video_script() for Gradio.
235
+ Returns storyboard text, JSON, and warnings.
236
+ """
237
+ req = VideoRequest(
238
+ brand=brand or "",
239
+ product=product or "",
240
+ audience=audience or "",
241
+ goal=goal or "",
242
+ blueprint_name=blueprint_name or "short_ad",
243
+ duration_sec=int(duration_sec) if duration_sec else 20,
244
+ platform_name=platform_name or "Instagram Reels",
245
+ style=style or "warm",
246
+ extra_context=extra_context or "",
247
+ )
248
+
249
+ # Now returns a VideoScriptResponse (plan + warnings)
250
+ resp = generate_video_script(req, debug_first=bool(debug_first))
251
+
252
+ # --- Build human-readable storyboard from structured beats ---
253
+ sb_lines: List[str] = []
254
+ for beat in resp.beats: # resp.beats is a list[VideoBeat]
255
+ sb_lines.append(
256
+ f"Beat {beat.beat_index + 1}: {beat.title} "
257
+ f"({beat.t_start}s – {beat.t_end}s)"
258
+ )
259
+ sb_lines.append(f" Voiceover: {beat.voiceover}")
260
+ sb_lines.append(f" On-screen: {beat.on_screen}")
261
+
262
+ sb_lines.append(" Shots:")
263
+ for shot in beat.shots:
264
+ sb_lines.append(f" • {shot}")
265
+
266
+ sb_lines.append(" B-roll:")
267
+ for br in beat.broll:
268
+ sb_lines.append(f" • {br}")
269
+
270
+ sb_lines.append(" Captions:")
271
+ for cap in beat.captions:
272
+ sb_lines.append(f" • {cap}")
273
+
274
+ sb_lines.append("") # blank line between beats
275
+
276
+ storyboard_text = "\n".join(sb_lines).strip() or "No beats generated."
277
+
278
+ # --- Warnings text ---
279
+ if resp.warnings:
280
+ warnings_text = "\n".join(f"- {w}" for w in resp.warnings)
281
+ else:
282
+ warnings_text = "No warnings. All beats parsed without fallback. ✅"
283
+
284
+ # --- JSON-ready object for download/integration ---
285
+ script_json: Dict[str, Any] = {
286
+ "plan": {
287
+ "blueprint_name": resp.plan.blueprint_name,
288
+ "duration_sec": resp.plan.duration_sec,
289
+ "platform_name": resp.plan.platform_name,
290
+ "style": resp.plan.style,
291
+ "beats": [
292
+ {
293
+ "index": b.beat_index,
294
+ "title": b.title,
295
+ "goal": b.goal,
296
+ "t_start": b.t_start,
297
+ "t_end": b.t_end,
298
+ }
299
+ for b in resp.plan.beats
300
+ ],
301
+ },
302
+ # Full beats payload with all fields (voiceover, shots, etc.)
303
+ "beats": [b.model_dump() for b in resp.beats],
304
+ "warnings": resp.warnings,
305
+ }
306
+
307
+ return storyboard_text, script_json, warnings_text
308
+
309
+
310
+
311
+ # ----- Gradio layout -----
312
+
313
+
314
+ def create_interface() -> gr.Blocks:
315
+ """
316
+ Create and return the Gradio Blocks interface.
317
+ """
318
+ with gr.Blocks(title="Marketeer – Copy & Video Script Generator") as demo:
319
+ gr.Markdown(
320
+ """
321
+ # Marketeer – Copy & Video Script Generator
322
+
323
+ Fill in your campaign details, generate a first draft, then refine it
324
+ in a single chat with your AI copywriter. Also generate short-form video
325
+ storyboards for your campaigns.
326
+ """
327
+ )
328
+
329
+ with gr.Tabs():
330
+ # --- Tab 1: Copy Chat (single chat interface) ---
331
+ with gr.Tab("Copy Chat"):
332
+ with gr.Row():
333
+ # LEFT COLUMN: Campaign setup
334
+ with gr.Column(scale=1):
335
+ gr.Markdown("### Campaign Setup")
336
+
337
+ brand = gr.Textbox(
338
+ label="Brand / Company",
339
+ placeholder="Brew Bliss Café",
340
+ )
341
+ product = gr.Textbox(
342
+ label="Product / Offer",
343
+ placeholder="signature cold brew",
344
+ )
345
+ audience = gr.Textbox(
346
+ label="Target audience",
347
+ placeholder=(
348
+ "young professionals who love coffee but hate waiting in line"
349
+ ),
350
+ )
351
+
352
+ # Campaign goal: preset dropdown + optional custom
353
+ goal_preset = gr.Dropdown(
354
+ label="Campaign goal",
355
+ choices=[
356
+ "Increase brand awareness",
357
+ "Lead generation",
358
+ "Drive website traffic",
359
+ "Promote in-store visits",
360
+ "Boost engagement",
361
+ "Announce a new product",
362
+ ],
363
+ value="Increase brand awareness",
364
+ )
365
+ goal_custom = gr.Textbox(
366
+ label="Custom goal (optional)",
367
+ placeholder="e.g. drive in-store visits this weekend",
368
+ lines=2,
369
+ )
370
+
371
+ platform_name = gr.Dropdown(
372
+ label="Platform",
373
+ choices=["Instagram", "Facebook", "LinkedIn", "Twitter"],
374
+ value="Instagram",
375
+ )
376
+ tone = gr.Dropdown(
377
+ label="Tone",
378
+ choices=[
379
+ "friendly",
380
+ "professional",
381
+ "energetic",
382
+ "storytelling",
383
+ ],
384
+ value="friendly",
385
+ )
386
+ cta_style = gr.Dropdown(
387
+ label="CTA style",
388
+ choices=["soft", "medium", "hard"],
389
+ value="soft",
390
+ )
391
+
392
+ extra_context = gr.Textbox(
393
+ label="Extra context (optional)",
394
+ placeholder="Mention that we have comfy seating and free Wi-Fi.",
395
+ lines=3,
396
+ )
397
+
398
+ generate_copy_btn = gr.Button(
399
+ "✨ Generate First Draft (and start chat)"
400
+ )
401
+
402
+ # RIGHT COLUMN: Chat + Feedback
403
+ with gr.Column(scale=2):
404
+ gr.Markdown("### Chat with your copywriter")
405
+
406
+ chatbox = gr.Chatbot(
407
+ label="Copy Chat (context-aware)",
408
+ height=320,
409
+ )
410
+ user_msg = gr.Textbox(
411
+ label="Your message",
412
+ placeholder=(
413
+ "Examples:\n"
414
+ "- 'Write a first post for this campaign.'\n"
415
+ "- 'Shorten this and keep the main message.'\n"
416
+ "- 'Adapt this for LinkedIn, more professional.'"
417
+ ),
418
+ lines=3,
419
+ )
420
+ with gr.Row():
421
+ send_btn = gr.Button("Send")
422
+ clear_btn = gr.Button("Clear Chat")
423
+
424
+ gr.Markdown("#### Feedback on the last reply")
425
+ fb_rating = gr.Radio(
426
+ label="How was the last AI reply?",
427
+ choices=["👍 Helpful", "👌 Okay", "👎 Needs improvement"],
428
+ value="👍 Helpful",
429
+ )
430
+ fb_text = gr.Textbox(
431
+ label="Feedback (optional)",
432
+ placeholder="What worked well? What should be improved?",
433
+ lines=3,
434
+ )
435
+ fb_submit = gr.Button("Submit feedback for last reply")
436
+ fb_status = gr.Markdown("")
437
+
438
+ # Wire first-draft generator (seeds chat only)
439
+ generate_copy_btn.click(
440
+ fn=_generate_first_copy_ui,
441
+ inputs=[
442
+ brand,
443
+ product,
444
+ audience,
445
+ goal_preset,
446
+ goal_custom,
447
+ platform_name,
448
+ tone,
449
+ cta_style,
450
+ extra_context,
451
+ ],
452
+ outputs=[chatbox],
453
+ )
454
+
455
+ # Wire chat send button
456
+ send_btn.click(
457
+ fn=_chat_copy_ui,
458
+ inputs=[
459
+ chatbox,
460
+ user_msg,
461
+ brand,
462
+ product,
463
+ audience,
464
+ goal_preset,
465
+ goal_custom,
466
+ platform_name,
467
+ tone,
468
+ cta_style,
469
+ extra_context,
470
+ ],
471
+ outputs=[chatbox, user_msg],
472
+ )
473
+
474
+ # Wire chat clear button
475
+ clear_btn.click(
476
+ fn=_clear_chat,
477
+ inputs=None,
478
+ outputs=[chatbox],
479
+ )
480
+
481
+ # Wire feedback button (linked to last assistant reply)
482
+ fb_submit.click(
483
+ fn=_submit_feedback_for_last_reply,
484
+ inputs=[
485
+ chatbox,
486
+ fb_rating,
487
+ fb_text,
488
+ brand,
489
+ platform_name,
490
+ goal_preset,
491
+ goal_custom,
492
+ ],
493
+ outputs=[fb_status],
494
+ )
495
+
496
+ # --- Tab 2: Video Script Generator (unchanged logic) ---
497
+ with gr.Tab("Video Script Generator"):
498
+ with gr.Row():
499
+ with gr.Column():
500
+ v_brand = gr.Textbox(
501
+ label="Brand / Company",
502
+ placeholder="Brew Bliss Café",
503
+ )
504
+ v_product = gr.Textbox(
505
+ label="Product",
506
+ placeholder="signature cold brew",
507
+ )
508
+ v_audience = gr.Textbox(
509
+ label="Target audience",
510
+ placeholder=(
511
+ "young professionals who love coffee but hate waiting in line"
512
+ ),
513
+ )
514
+ v_goal = gr.Textbox(
515
+ label="Campaign goal",
516
+ placeholder="drive in-store visits this weekend",
517
+ )
518
+
519
+ blueprint_name = gr.Dropdown(
520
+ label="Blueprint",
521
+ choices=["short_ad", "ugc_review", "how_to"],
522
+ value="short_ad",
523
+ )
524
+ duration_sec = gr.Slider(
525
+ label="Video duration (seconds)",
526
+ minimum=5,
527
+ maximum=60,
528
+ step=1,
529
+ value=20,
530
+ )
531
+ platform_name_v = gr.Textbox(
532
+ label="Platform label (for prompt)",
533
+ value="Instagram Reels",
534
+ )
535
+ style = gr.Textbox(
536
+ label="Style",
537
+ value="warm and energetic",
538
+ )
539
+ extra_context_v = gr.Textbox(
540
+ label="Extra context (optional)",
541
+ placeholder=(
542
+ "Focus on escaping the grind and enjoying a chilled moment."
543
+ ),
544
+ lines=3,
545
+ )
546
+ debug_first = gr.Checkbox(
547
+ label="Print raw first beat to server logs (debug)",
548
+ value=False,
549
+ )
550
+
551
+ generate_video_btn = gr.Button("Generate Video Script")
552
+
553
+ with gr.Column():
554
+ storyboard = gr.Textbox(
555
+ label="Storyboard (per beat)",
556
+ lines=18,
557
+ )
558
+ warnings_box = gr.Textbox(
559
+ label="Warnings",
560
+ lines=6,
561
+ )
562
+ script_json = gr.JSON(
563
+ label="Full script JSON (for download/integration)",
564
+ )
565
+
566
+ generate_video_btn.click(
567
+ fn=_generate_video_ui,
568
+ inputs=[
569
+ v_brand,
570
+ v_product,
571
+ v_audience,
572
+ v_goal,
573
+ blueprint_name,
574
+ duration_sec,
575
+ platform_name_v,
576
+ style,
577
+ extra_context_v,
578
+ debug_first,
579
+ ],
580
+ outputs=[storyboard, script_json, warnings_box],
581
+ )
582
+
583
+ return demo
ui/gradio_ui_1.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio UI for Marketeer (copy + video script).
3
+
4
+ This file wires the core logic into a simple web interface
5
+ with two tabs:
6
+ - Copy Generator
7
+ - Video Script Generator
8
+ """
9
+
10
+ from typing import Any, Dict, List
11
+
12
+ import gradio as gr
13
+
14
+ from core_logic.copy_pipeline import CopyRequest, generate_copy
15
+ from core_logic.video_pipeline import VideoRequest, generate_video_script
16
+
17
+
18
+ # ----- Backend wrapper functions for Gradio -----
19
+
20
+
21
+ def _generate_copy_ui(
22
+ brand: str,
23
+ product: str,
24
+ audience: str,
25
+ goal: str,
26
+ platform_name: str,
27
+ tone: str,
28
+ cta_style: str,
29
+ extra_context: str,
30
+ ):
31
+ """
32
+ Wrapper around generate_copy() for Gradio.
33
+ Returns final_text, raw_text, audit_text in that order.
34
+ """
35
+ req = CopyRequest(
36
+ brand=brand or "",
37
+ product=product or "",
38
+ audience=audience or "",
39
+ goal=goal or "",
40
+ platform_name=platform_name or "Instagram",
41
+ tone=tone or "friendly",
42
+ cta_style=cta_style or "soft",
43
+ extra_context=extra_context or "",
44
+ )
45
+
46
+ resp = generate_copy(req)
47
+
48
+ # Pretty-print audit log
49
+ if resp.audit:
50
+ audit_lines = []
51
+ for item in resp.audit:
52
+ rule = item.get("rule", "unknown")
53
+ audit_lines.append(f"- {rule}: {item}")
54
+ audit_text = "\n".join(audit_lines)
55
+ else:
56
+ audit_text = "No edits were needed. ✅"
57
+
58
+ # RETURN IN ORDER: final_copy, raw_output, audit_log
59
+ return resp.final, resp.raw, audit_text
60
+
61
+
62
+ def _generate_video_ui(
63
+ brand: str,
64
+ product: str,
65
+ audience: str,
66
+ goal: str,
67
+ blueprint_name: str,
68
+ duration_sec: int,
69
+ platform_name: str,
70
+ style: str,
71
+ extra_context: str,
72
+ debug_first: bool,
73
+ ):
74
+ """
75
+ Wrapper around generate_video_script() for Gradio.
76
+ Returns storyboard_text, script_json, warnings_text in that order.
77
+ """
78
+ req = VideoRequest(
79
+ brand=brand or "",
80
+ product=product or "",
81
+ audience=audience or "",
82
+ goal=goal or "",
83
+ blueprint_name=blueprint_name or "short_ad",
84
+ duration_sec=int(duration_sec) if duration_sec else 20,
85
+ platform_name=platform_name or "Instagram Reels",
86
+ style=style or "warm",
87
+ extra_context=extra_context or "",
88
+ )
89
+
90
+ resp = generate_video_script(req, debug_first=bool(debug_first))
91
+
92
+ # storyboard text (same as before)
93
+ sb_lines = []
94
+ for block in resp.beats:
95
+ sb_lines.append(
96
+ f"Beat {block['beat_index'] + 1}: {block['beat_title']} "
97
+ f"({block['t_start']}s – {block['t_end']}s)"
98
+ )
99
+ sb_lines.append(f" Voiceover: {block['voiceover']}")
100
+ sb_lines.append(f" On-screen: {block['on_screen']}")
101
+ sb_lines.append(" Shots:")
102
+ for shot in block["shots"]:
103
+ sb_lines.append(f" • {shot}")
104
+ sb_lines.append(" B-roll:")
105
+ for br in block["broll"]:
106
+ sb_lines.append(f" • {br}")
107
+ sb_lines.append(" Captions:")
108
+ for cap in block["captions"]:
109
+ sb_lines.append(f" • {cap}")
110
+ sb_lines.append("")
111
+
112
+ storyboard_text = "\n".join(sb_lines).strip() or "No beats generated."
113
+
114
+ # warnings text
115
+ if resp.warnings:
116
+ warnings_text = "\n".join(f"- {w}" for w in resp.warnings)
117
+ else:
118
+ warnings_text = "No warnings. All beats parsed without fallback. ✅"
119
+
120
+ # JSON object
121
+ script_json = {
122
+ "plan": {
123
+ "blueprint_name": resp.plan.blueprint_name,
124
+ "duration_sec": resp.plan.duration_sec,
125
+ "platform_name": resp.plan.platform_name,
126
+ "style": resp.plan.style,
127
+ "beats": [
128
+ {
129
+ "index": b.index,
130
+ "title": b.title,
131
+ "goal": b.goal,
132
+ "t_start": b.t_start,
133
+ "t_end": b.t_end,
134
+ }
135
+ for b in resp.plan.beats
136
+ ],
137
+ },
138
+ "beats": resp.beats,
139
+ "warnings": resp.warnings,
140
+ }
141
+
142
+ # RETURN IN ORDER: storyboard, json, warnings
143
+ return storyboard_text, script_json, warnings_text
144
+
145
+
146
+ # ----- Gradio layout -----
147
+
148
+
149
+ def create_interface() -> gr.Blocks:
150
+ """
151
+ Create and return the Gradio Blocks interface.
152
+ """
153
+ with gr.Blocks(title="Marketeer – Copy & Video Script Generator") as demo:
154
+ gr.Markdown(
155
+ """
156
+ # Marketeer – Copy & Video Script Generator
157
+
158
+ Generate platform-aware marketing copy and short-form video scripts,
159
+ powered by your patched Gemma-based backend.
160
+ """
161
+ )
162
+
163
+ with gr.Tabs():
164
+ # --- Tab 1: Copy Generator ---
165
+ with gr.Tab("Copy Generator"):
166
+ with gr.Row():
167
+ with gr.Column():
168
+ brand = gr.Textbox(
169
+ label="Brand / Company",
170
+ placeholder="Brew Bliss Café",
171
+ )
172
+ product = gr.Textbox(
173
+ label="Product / Offer",
174
+ placeholder="signature cold brew",
175
+ )
176
+ audience = gr.Textbox(
177
+ label="Target audience",
178
+ placeholder="young professionals who love coffee but hate waiting in line",
179
+ )
180
+ goal = gr.Textbox(
181
+ label="Campaign goal",
182
+ placeholder="drive in-store visits this weekend",
183
+ )
184
+
185
+ platform_name = gr.Dropdown(
186
+ label="Platform",
187
+ choices=["Instagram", "Facebook", "LinkedIn", "Twitter"],
188
+ value="Instagram",
189
+ )
190
+ tone = gr.Dropdown(
191
+ label="Tone",
192
+ choices=["friendly", "professional", "energetic", "storytelling"],
193
+ value="friendly",
194
+ )
195
+ cta_style = gr.Dropdown(
196
+ label="CTA style",
197
+ choices=["soft", "medium", "hard"],
198
+ value="soft",
199
+ )
200
+
201
+ extra_context = gr.Textbox(
202
+ label="Extra context (optional)",
203
+ placeholder="Mention that we have comfy seating and free Wi-Fi.",
204
+ lines=3,
205
+ )
206
+
207
+ generate_copy_btn = gr.Button("Generate Copy")
208
+
209
+ with gr.Column():
210
+ final_copy = gr.Textbox(
211
+ label="Final Copy",
212
+ lines=10,
213
+ )
214
+ audit_log = gr.Textbox(
215
+ label="Audit Log",
216
+ lines=8,
217
+ )
218
+ raw_output = gr.Textbox(
219
+ label="Raw Model Output (debug)",
220
+ lines=8,
221
+ visible=False, # flip to True if you want to see raw text
222
+ )
223
+
224
+ # Wire copy button
225
+ generate_copy_btn.click(
226
+ fn=_generate_copy_ui,
227
+ inputs=[
228
+ brand,
229
+ product,
230
+ audience,
231
+ goal,
232
+ platform_name,
233
+ tone,
234
+ cta_style,
235
+ extra_context,
236
+ ],
237
+ outputs=[final_copy, raw_output, audit_log],
238
+ )
239
+
240
+ # --- Tab 2: Video Script Generator ---
241
+ with gr.Tab("Video Script Generator"):
242
+ with gr.Row():
243
+ with gr.Column():
244
+ v_brand = gr.Textbox(
245
+ label="Brand / Company",
246
+ placeholder="Brew Bliss Café",
247
+ )
248
+ v_product = gr.Textbox(
249
+ label="Product",
250
+ placeholder="signature cold brew",
251
+ )
252
+ v_audience = gr.Textbox(
253
+ label="Target audience",
254
+ placeholder="young professionals who love coffee but hate waiting in line",
255
+ )
256
+ v_goal = gr.Textbox(
257
+ label="Campaign goal",
258
+ placeholder="drive in-store visits this weekend",
259
+ )
260
+
261
+ blueprint_name = gr.Dropdown(
262
+ label="Blueprint",
263
+ choices=["short_ad", "ugc_review", "how_to"],
264
+ value="short_ad",
265
+ )
266
+ duration_sec = gr.Slider(
267
+ label="Video duration (seconds)",
268
+ minimum=5,
269
+ maximum=60,
270
+ step=1,
271
+ value=20,
272
+ )
273
+ platform_name_v = gr.Textbox(
274
+ label="Platform label (for prompt)",
275
+ value="Instagram Reels",
276
+ )
277
+ style = gr.Textbox(
278
+ label="Style",
279
+ value="warm and energetic",
280
+ )
281
+ extra_context_v = gr.Textbox(
282
+ label="Extra context (optional)",
283
+ placeholder="Focus on escaping the grind and enjoying a chilled moment.",
284
+ lines=3,
285
+ )
286
+ debug_first = gr.Checkbox(
287
+ label="Print raw first beat to server logs (debug)",
288
+ value=False,
289
+ )
290
+
291
+ generate_video_btn = gr.Button("Generate Video Script")
292
+
293
+ with gr.Column():
294
+ storyboard = gr.Textbox(
295
+ label="Storyboard (per beat)",
296
+ lines=18,
297
+ )
298
+ warnings_box = gr.Textbox(
299
+ label="Warnings",
300
+ lines=6,
301
+ )
302
+ script_json = gr.JSON(
303
+ label="Full script JSON (for download/integration)",
304
+ )
305
+
306
+ generate_video_btn.click(
307
+ fn=_generate_video_ui,
308
+ inputs=[
309
+ v_brand,
310
+ v_product,
311
+ v_audience,
312
+ v_goal,
313
+ blueprint_name,
314
+ duration_sec,
315
+ platform_name_v,
316
+ style,
317
+ extra_context_v,
318
+ debug_first,
319
+ ],
320
+ outputs=[storyboard, script_json, warnings_box],
321
+ )
322
+
323
+ return demo