feat: add selective image roast analysis#183
Conversation
Reviewer's GuideAdds a configurable image roast (image summary) feature that selects candidate chat images (excluding bot reports and emojis), runs them through a visual LLM for roast-style summaries, and renders them as a separate “image moments” section in the scrapbook reports, controlled via new config flags and prompt templates. Sequence diagram for daily analysis with selective image roastsequenceDiagram
actor User
participant Bot as BotService
participant AnalysisApp as AnalysisApplicationService
participant Config as ConfigManager
participant Stats as StatisticsService
participant LLM as VisualLLMProvider
participant ReportGen as ReportingGenerator
User->>Bot: request_daily_report()
Bot->>AnalysisApp: execute_daily_analysis()
Note over AnalysisApp: existing text analysis flow omitted
AnalysisApp->>Config: get_image_summary_enabled()
Config-->>AnalysisApp: enabled_flag
alt image_summary_enabled
AnalysisApp->>Config: get_bot_self_ids()
Config-->>AnalysisApp: config_bot_ids
AnalysisApp->>AnalysisApp: read_runtime_bot_ids()
AnalysisApp->>Config: get_max_image_summaries()
Config-->>AnalysisApp: max_image_summaries
AnalysisApp->>Stats: extract_image_summaries(unified_messages, limit, bot_self_ids)
Stats->>Stats: filter_bot_daily_report_images()
Stats->>Stats: filter_emoji_like_images()
Stats-->>AnalysisApp: raw_image_summaries
loop for each image_summary
AnalysisApp->>Config: get_image_summary_prompt()
Config-->>AnalysisApp: prompt_template
AnalysisApp->>LLM: llm_generate(provider_id, prompt, image_url, system_prompt)
LLM-->>AnalysisApp: completion_text
AnalysisApp->>AnalysisApp: decide_keep_or_skip()
alt keep
AnalysisApp->>AnalysisApp: set model_summary and description
else skip
AnalysisApp->>AnalysisApp: drop image
end
end
AnalysisApp->>AnalysisApp: statistics.image_summaries = kept_items
else image_summary_disabled
AnalysisApp->>AnalysisApp: statistics.image_summaries = []
end
AnalysisApp-->>Bot: analysis_result (includes image_summaries)
Bot->>ReportGen: _prepare_render_data(analysis_result)
ReportGen->>ReportGen: build_image_summary_html(image_summaries)
ReportGen-->>Bot: scrapbook_html
Bot-->>User: send_scrapbook_report(scrapbook_html)
Class diagram for new image summary data model and servicesclassDiagram
class ImageSummaryItem {
+str url
+str sender
+str sender_id
+str description
+str model_summary
}
class StatisticsService {
+extract_image_summaries(messages, limit, bot_self_ids) list~ImageSummaryItem~
+_is_bot_daily_report_image(msg, bot_self_ids) bool
+_is_emoji_like_image(raw_data) bool
}
class AnalysisApplicationService {
+execute_daily_analysis(...)
+_enrich_image_summaries(image_summaries, unified_msg_origin, keep_limit) list
}
class ConfigManager {
+get_image_summary_enabled() bool
+get_max_image_summaries() int
+get_image_summary_prompt(style) str
+get_max_golden_quotes() int
+get_bot_self_ids() list
}
class ReportingGenerator {
+_prepare_render_data(analysis_result, stats, activity_viz, chat_quality_review) dict
}
class LLMAnalyzerContext {
+llm_generate(chat_provider_id, prompt, image_urls, system_prompt) LLMResponse
}
class LLMResponse {
+str completion_text
}
AnalysisApplicationService --> StatisticsService : uses
AnalysisApplicationService --> ConfigManager : reads_config
AnalysisApplicationService --> LLMAnalyzerContext : calls
StatisticsService --> ImageSummaryItem : creates
ReportingGenerator --> ConfigManager : reads_limits
ReportingGenerator --> ImageSummaryItem : renders
LLMAnalyzerContext --> LLMResponse : returns
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The
_is_bot_daily_report_imageheuristic hardcodes a fairly long list of sender/text markers; consider moving these marker lists into configuration (or at least a module-level constant) so they can be tuned per deployment without code changes. - The new image summary flow uses very loose typing (
list[Any], duck-typeditemin_enrich_image_summariesand generator HTML); tightening this toImageSummaryItem | dict(or a dedicated protocol) would make it easier to catch misuse and refactors at compile-time.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `_is_bot_daily_report_image` heuristic hardcodes a fairly long list of sender/text markers; consider moving these marker lists into configuration (or at least a module-level constant) so they can be tuned per deployment without code changes.
- The new image summary flow uses very loose typing (`list[Any]`, duck-typed `item` in `_enrich_image_summaries` and generator HTML); tightening this to `ImageSummaryItem | dict` (or a dedicated protocol) would make it easier to catch misuse and refactors at compile-time.
## Individual Comments
### Comment 1
<location path="src/infrastructure/reporting/generators.py" line_range="831-833" />
<code_context>
+ # 生成图片锐评HTML:独立于金句区块,避免和最后一条文字锐评粘连
+ image_summary_html = ""
+ image_summaries = analysis_result.get("image_summaries") or []
+ max_image_summaries = min(
+ self.config_manager.get_max_golden_quotes(),
+ getattr(self.config_manager, "get_max_image_summaries", lambda: 5)(),
+ )
+ display_image_summaries = image_summaries[:max_image_summaries]
</code_context>
<issue_to_address>
**question (bug_risk):** Coupling the max image summaries to `max_golden_quotes` may unintentionally constrain image sections.
Because `max_image_summaries` is computed as `min(max_golden_quotes, get_max_image_summaries())`, a low `max_golden_quotes` will cap images even if `max_image_summaries` is configured higher. If this coupling is intentional, please document it in the config semantics; otherwise, consider using only `get_max_image_summaries()` here so image limits are independent of golden quotes:
```python
max_image_summaries = getattr(self.config_manager, "get_max_image_summaries", lambda: 5)()
```
</issue_to_address>
### Comment 2
<location path="src/application/services/analysis_application_service.py" line_range="370" />
<code_context>
+ text = (getattr(resp, "completion_text", "") or "").strip()
+ normalized = text.strip().lower().strip("`*_ -。.!!")
+ if not text or normalized.startswith("skip") or normalized in {"跳过", "不保留", "忽略"}:
+ logger.info(f"图片锐评筛选跳过普通图片: {url}")
+ continue
+ item.model_summary = text[:160]
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Logging full image URLs for skipped pictures might be too verbose or leak sensitive links.
To reduce this risk, consider omitting the URL, logging only a derived ID (e.g., hash or index), or lowering the log level. For example:
```python
logger.debug("图片锐评筛选跳过普通图片", extra={"image_idx": idx})
```
Suggested implementation:
```python
if not text or normalized.startswith("skip") or normalized in {"跳过", "不保留", "忽略"}:
logger.debug("图片锐评筛选跳过普通图片")
continue
```
If the surrounding loop exposes an `idx` or other stable identifier, you can further adjust the log to include it without leaking the URL, for example:
- Change the call to `logger.debug("图片锐评筛选跳过普通图片", extra={"image_idx": idx})` when `idx` is available.
- Alternatively, if you prefer a hash-based ID, compute a hash from `url` (e.g., using `hashlib.sha256(url.encode()).hexdigest()`) and log that instead: `extra={"image_id": image_id}`.
These adjustments should be made where the loop variables are defined.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| max_image_summaries = min( | ||
| self.config_manager.get_max_golden_quotes(), | ||
| getattr(self.config_manager, "get_max_image_summaries", lambda: 5)(), |
There was a problem hiding this comment.
question (bug_risk): Coupling the max image summaries to max_golden_quotes may unintentionally constrain image sections.
Because max_image_summaries is computed as min(max_golden_quotes, get_max_image_summaries()), a low max_golden_quotes will cap images even if max_image_summaries is configured higher. If this coupling is intentional, please document it in the config semantics; otherwise, consider using only get_max_image_summaries() here so image limits are independent of golden quotes:
max_image_summaries = getattr(self.config_manager, "get_max_image_summaries", lambda: 5)()| text = (getattr(resp, "completion_text", "") or "").strip() | ||
| normalized = text.strip().lower().strip("`*_ -。.!!") | ||
| if not text or normalized.startswith("skip") or normalized in {"跳过", "不保留", "忽略"}: | ||
| logger.info(f"图片锐评筛选跳过普通图片: {url}") |
There was a problem hiding this comment.
🚨 suggestion (security): Logging full image URLs for skipped pictures might be too verbose or leak sensitive links.
To reduce this risk, consider omitting the URL, logging only a derived ID (e.g., hash or index), or lowering the log level. For example:
logger.debug("图片锐评筛选跳过普通图片", extra={"image_idx": idx})Suggested implementation:
if not text or normalized.startswith("skip") or normalized in {"跳过", "不保留", "忽略"}:
logger.debug("图片锐评筛选跳过普通图片")
continueIf the surrounding loop exposes an idx or other stable identifier, you can further adjust the log to include it without leaking the URL, for example:
- Change the call to
logger.debug("图片锐评筛选跳过普通图片", extra={"image_idx": idx})whenidxis available. - Alternatively, if you prefer a hash-based ID, compute a hash from
url(e.g., usinghashlib.sha256(url.encode()).hexdigest()) and log that instead:extra={"image_id": image_id}.
These adjustments should be made where the loop variables are defined.
|
想法挺好,能不能看看实际效果图 |
|
我最近期中考试,等有时间会审一下的,很好的 PR,感谢贡献 |
* refactor:优化了移动端直接使用浏览器打开HTML时的UI * feat:将所有模板中的base64编码图片转换为链接拉取以降低源文件大小
|
Thanks for the work on the image roast flow — the separation from text golden quotes looks useful. One configuration detail I’d like to confirm before merge: the code now looks up This would help users understand what needs to be configured for image input support and avoid silent fallback/misconfiguration surprises. |
|
巡检时看了一下这组改动,整体方向很有价值:图片锐评默认关闭、候选图先过滤再走视觉模型、并且增加了 合入前建议再补两处小修,能降低后续维护风险:
另外,之前 bot 提到的“跳过图片时不要记录完整 URL”和“图片数量不要受金句数量限制”看起来在最新提交里已经基本处理了(debug 级别日志、不再 |
|
定时协作巡检看到这个 PR,整体方向很有价值,尤其是把图片名场面做成默认关闭、候选数/展示数分离、并发限流都比较稳。 我这边快速过了一遍 diff,有两个小点供参考:
没有做阻塞性操作,只是巡检建议。 |







改动
SKIP后不进入日报校验
python3 -m json.tool _conf_schema.jsonpython3 -m py_compile src/domain/models/data_models.py src/domain/services/statistics_service.py src/infrastructure/config/config_manager.py src/application/services/analysis_application_service.py src/infrastructure/reporting/generators.pygit diff --checkSummary by Sourcery
Add image roast extraction, analysis, and rendering to daily group chat reports.
New Features:
Enhancements: